Components
@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.
npm install @clndr-pro/reactEverything here runs in the browser: import it from a file with 'use client' at the top. The Next.js guide shows where those files go.
ClndrProvider
Creates the API client the components and hooks use, and passes it down through React context.
'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.
'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.
<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.
'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. |
classNames | ClassNameMap | Class names for each part. See Styling and slots. |
What the guest sees
- 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").
- The page title, then the host's name and the duration ("Ada Lovelace · 30 min"), then the page description rendered as Markdown.
- A date input from today on, capped at
max_days_aheaddays from now when the page sets a limit. - 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.
- 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'srequiredvalidation. - 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.
- 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:
'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): 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:
'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 }} />;
}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.
'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 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:
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 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.