# Components

Reference for ClndrProvider, BookingInline, BookingForm and BookingModal in @clndr-pro/react — props, what the default booking flow does, prefilling guests, and using several accounts on one site.

`@clndr-pro/react` has one provider and three components. The components are the same booking flow in different wrappers, so most of this page is about one of them.

```bash
npm install @clndr-pro/react
```

Everything here runs in the browser: import it from a file with `'use client'` at the top. The [Next.js guide](/nextjs) shows where those files go.

## ClndrProvider

Creates the API client the components and hooks use, and passes it down through React context.

```tsx title="app/providers.tsx"
'use client';

import { ClndrProvider } from '@clndr-pro/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ClndrProvider publishableKey={process.env.NEXT_PUBLIC_CLNDR_PUBLISHABLE_KEY!}>
      {children}
    </ClndrProvider>
  );
}
```

| Prop             | Type        |                                                                                                                        |
| ---------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
| `publishableKey` | `string`    | A `clndr_pk_` key. Required unless you pass `client`.                                                                  |
| `client`         | `Clndr`     | An already-built client. Takes priority over `publishableKey`.                                                         |
| `baseUrl`        | `string`    | API host. Defaults to `https://www.clndr.pro` from 0.2.1. Only needed on older versions, or against a local clndr.pro. |
| `children`       | `ReactNode` |                                                                                                                        |

The provider throws if it gets neither `publishableKey` nor `client`, and every component or hook throws `useClndr must be used inside <ClndrProvider>` when there's no provider above it.

### Your own client

Pass `client` when you need control over how requests are made: a custom `fetch` for logging or tests, or a different base URL. Build it once, outside the component. The provider memoises on the client's identity, so a new `Clndr` on every render recreates the context each time.

```tsx title="components/debug-booking.tsx"
'use client';

import { Clndr, ClndrProvider, BookingInline } from '@clndr-pro/react';

// Built once at module scope: ClndrProvider memoises on the client's identity.
const client = new Clndr({
  apiKey: process.env.NEXT_PUBLIC_CLNDR_PUBLISHABLE_KEY!,
  fetch: (input, init) => {
    console.debug('clndr request', input);
    return fetch(input, init);
  },
});

export function DebugBooking() {
  return (
    <ClndrProvider client={client}>
      <BookingInline slug="intro-call" />
    </ClndrProvider>
  );
}
```

### Several accounts on one site

A key belongs to one clndr.pro account, and a slug is only unique within an account. To book with two people who each have their own account, give each section its own provider. Components use the nearest one.

```tsx
<ClndrProvider publishableKey={process.env.NEXT_PUBLIC_CLNDR_KEY_ADA!}>
  <BookingInline slug="intro-call" />
</ClndrProvider>

<ClndrProvider publishableKey={process.env.NEXT_PUBLIC_CLNDR_KEY_GRACE!}>
  <BookingInline slug="intro-call" />
</ClndrProvider>
```

## BookingInline and BookingForm

`BookingInline` renders `BookingForm` and nothing else; use whichever name reads better in your code. Both take the same props.

```tsx title="app/book/booking-widget.tsx"
'use client';

import { BookingInline } from '@clndr-pro/react';

export function BookingWidget() {
  return <BookingInline slug="intro-call" onBooked={(id) => console.log('booked', id)} />;
}
```

| Prop         | Type                                |                                                                                                  |
| ------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------ |
| `slug`       | `string`                            | The booking page's slug. Required.                                                               |
| `prefill`    | `{ name?: string; email?: string }` | Initial values for the guest's name and email. The guest can still edit them.                    |
| `onBooked`   | `(bookingId: string) => void`       | Called after the booking is created, with its `id`.                                              |
| `components` | `ComponentSlots`                    | Your own `Button`, `Input`, `Label` and other elements. See [Styling and slots](/react/styling). |
| `classNames` | `ClassNameMap`                      | Class names for each part. See [Styling and slots](/react/styling).                              |

### What the guest sees

1. While the page loads: "Loading…". If the slug doesn't exist on the key's account, or the page is inactive, the API's error message ("Booking page not found").
2. The page title, then the host's name and the duration ("Ada Lovelace · 30 min"), then the page description rendered as Markdown.
3. A date input from today on, capped at `max_days_ahead` days from now when the page sets a limit.
4. After a date is picked, the open times for that day as buttons, in the guest's local time. "No available slots for this date." when there are none.
5. After a time is picked, a form: name, email, and the host's questions in their order. Required questions are marked with `*` and use the browser's `required` validation.
6. **Confirm** creates the booking. If that fails (someone else took the time, say), the API's message appears above the buttons. **Back** returns to the times.
7. A success card. *Direct* pages say "Booking confirmed" and that a confirmation email is coming. *Approval* pages say the request was submitted and the guest will hear once it's approved (the heading reads "Request sent" from 0.2.1; 0.2.0 says "Booking confirmed" for both).

Question types map to inputs like this from 0.2.1: `textarea` gets a text area, `email` an email input, `phone` a `tel` input, `select` a `<select>` with the question's options, `checkbox` a checkbox whose answer is "Yes" or "No". In 0.2.0 everything except `textarea` and `email` renders as a plain text input. The dashboard only creates `text`, `email`, `phone` and `textarea` questions today, so this mostly matters for `phone`.

The API doesn't check that required questions were answered; the form's `required` attributes do. If you build your own form, validate them yourself.

### After booking: onBooked

`onBooked` fires once, with the new booking's id, after the API returns it. The success card renders at the same moment. Common uses:

```tsx
'use client';

import { useRouter } from 'next/navigation';
import { BookingInline } from '@clndr-pro/react';

export function BookingWidget() {
  const router = useRouter();
  return (
    <BookingInline
      slug="intro-call"
      onBooked={(bookingId) => {
        // Send your analytics event here, then move on.
        router.push(`/thanks?booking=${bookingId}`);
      }}
    />
  );
}
```

The id is all the browser gets. To show booking details on the thanks page, read the booking on your server with a secret key ([Get a booking](/api-reference/operations/getBooking)): a publishable key can't read bookings, by design.

### Prefill from your own user

If the guest is signed in to your site, pass what you know. Read the user on the server and hand plain values to the client component:

```tsx title="app/onboarding/book-with-account.tsx"
'use client';

import { BookingInline } from '@clndr-pro/react';

export function BookWithAccount({ user }: { user: { name: string; email: string } }) {
  return <BookingInline slug="onboarding-call" prefill={{ name: user.name, email: user.email }} />;
}
```

```tsx title="app/onboarding/page.tsx"
import { getCurrentUser } from '@/lib/auth'; // your auth library
import { BookWithAccount } from './book-with-account';

export default async function OnboardingPage() {
  const user = await getCurrentUser();
  return <BookWithAccount user={{ name: user.name, email: user.email }} />;
}
```

## BookingModal

The same flow in a fixed-position overlay. It closes on Escape and on a click outside the panel, and closes itself 2.5 seconds after a successful booking so the guest sees the confirmation first.

```tsx title="components/book-button.tsx"
'use client';

import { useState } from 'react';
import { BookingModal } from '@clndr-pro/react';

export function BookButton() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Book a call
      </button>
      <BookingModal slug="intro-call" open={open} onOpenChange={setOpen} />
    </>
  );
}
```

Every `BookingInline` prop, plus:

| Prop               | Type                      |                                                                                 |
| ------------------ | ------------------------- | ------------------------------------------------------------------------------- |
| `open`             | `boolean`                 | Whether the modal is showing. Required.                                         |
| `onOpenChange`     | `(open: boolean) => void` | Called with `false` on Escape, an outside click, and after a booking. Required. |
| `overlayClassName` | `string`                  | Replaces the built-in overlay styles (a 50% black backdrop, centred panel).     |
| `contentClassName` | `string`                  | Replaces the built-in panel styles (white, 8px radius, 560px wide).             |

The built-in styles are inline and fixed: the panel stays white in dark mode until you pass `contentClassName`. It also doesn't trap focus or lock page scroll. If your site has a design system, render `BookingForm` inside your own dialog instead: [Booking dialog with shadcn](/tutorials/shadcn-booking-dialog) shows how, with a drawer on phones.

## Types

The package re-exports the SDK's types and classes, so one import covers a component file:

```ts
import {
  Clndr,
  ClndrError,
  type Booking,
  type BookingPage,
  type BookingPageDetail,
  type BookingQuestion,
  type CreateBookingInput,
  type TimeSlot,
  type UserProfile,
  type BookingFormProps,
  type BookingModalProps,
  type ComponentSlots,
  type ClassNameMap,
} from '@clndr-pro/react';
```

[Data types](/types) describes every field. For a flow these components can't express (a month calendar, slots grouped by morning and afternoon, a multi-step wizard), use the [hooks](/react/hooks).
