Styling and slots
BookingForm, BookingInline and BookingModal ship plain HTML with a clndr-* class on every part and no CSS at all. Two props change that:
componentsswaps the elements themselves: yourButton,Input,Label.classNamesswaps the class on each part.
Use both with a component library, classNames alone with Tailwind, or neither and write CSS against the default classes.
Slots: the components prop
| Slot | Default | Renders |
|---|---|---|
Root | <div> | The outer wrapper, in every state |
Card | <div> | The details form, and the success message |
Heading | <h2> | The page title, and the success heading |
Muted | <div> | Host and duration, the picked time, loading and empty messages |
Error | <div> | Load and booking errors |
Button | <button> | Every button: time slots, Back and Confirm |
Input | <input> | The date input, name, email, and text / email / phone questions |
Textarea | <textarea> | textarea questions |
Select | <select> | select questions, with the options as <option> children. 0.2.1+ |
Label | <label> | Every field label |
MarkdownRenderer | Lazy-loaded react-markdown | The page description. Gets { content, className }. |
Each slot receives the native props for its element (className, onClick, value, htmlFor, …), so any component that forwards them works. The checkbox for checkbox questions is always a native <input type="checkbox">: design-system Inputs are text fields.
One Button slot covers three kinds of button, told apart only by class. To make time slots outlined while Confirm stays solid, give them different classNames (below) rather than different components.
Class names: the classNames prop
A class you pass replaces the default for that part; it isn't appended. Pass input: 'w-full' and the clndr-input class is gone from inputs.
| Key | Default | On |
|---|---|---|
root | clndr-root | Root |
heading | clndr-heading | Heading |
muted | clndr-muted | Muted |
error | clndr-error | Error |
description | clndr-description | The wrapper MarkdownRenderer draws |
label | clndr-label | Every Label |
dateInput | clndr-date-input | The date Input |
slotGrid | clndr-slot-grid | The <div> around the time buttons |
slotButton | clndr-slot-btn | Each time Button |
card | clndr-card | Card |
input | clndr-input | Name, email and question Inputs |
textarea | clndr-textarea | Textarea |
select | same as input | Select. 0.2.1+ |
checkbox | clndr-checkbox | Checkbox questions. 0.2.1+ |
buttonSecondary | clndr-btn clndr-btn-secondary | Back |
buttonPrimary | clndr-btn clndr-btn-primary | Confirm |
slotButtonSelected (clndr-slot-btn--selected) is in the type but nothing uses it yet: picking a time moves straight on to the form, so there's no selected state to draw. The row holding Back and Confirm has an inline display: flex; gap: 8px you can't override from classNames.
shadcn/ui
With shadcn's Button, Input, Label and Textarea, the form matches the rest of a shadcn app, including dark mode:
npx shadcn@latest add button input label textarea'use client';
import { BookingForm, type BookingFormProps } from '@clndr-pro/react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
/** The SDK's booking flow, drawn with your shadcn components. */
export function ShadcnBookingForm(props: Omit<BookingFormProps, 'components' | 'classNames'>) {
return (
<BookingForm
{...props}
components={{ Button, Input, Label, Textarea }}
classNames={{
root: 'space-y-5',
heading: 'text-lg font-semibold',
muted: 'text-sm text-muted-foreground',
label: 'mb-2 block',
dateInput: 'w-full',
slotGrid: 'mt-4 grid grid-cols-3 gap-2',
slotButton: 'bg-background text-foreground border hover:bg-accent hover:text-accent-foreground',
card: 'space-y-4',
buttonSecondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
error: 'text-sm text-destructive',
description: 'text-sm text-muted-foreground',
}}
/>
);
}shadcn's Button renders its default (solid) variant here, because the slot can't pick a variant. The slotButton and buttonSecondary classes turn that into outline and secondary looks: shadcn merges class names with tailwind-merge, so bg-background wins over the variant's bg-primary. If your Button doesn't merge classes, wrap it instead: Button: (props) => <Button variant="outline" {...props} /> makes every button outlined.
From 0.2.1 you can pass a Select slot too. shadcn's Select is a Radix component with a different API from <select> (onValueChange, no <option> children), so it doesn't fit the slot directly; a styled native select does. classNames.select takes input's classes by default, which is usually what you want.
For a calendar, times grouped by day, or a layout these slots can't reach, drop down to the hooks: the shadcn booking widget tutorial builds one with shadcn's Calendar.
Plain CSS
Without Tailwind, keep the default classes and style them. This stylesheet gives a clean, neutral form; change the variables at the top to match your site:
.clndr-root {
--clndr-fg: #18181b;
--clndr-muted: #71717a;
--clndr-border: #e4e4e7;
--clndr-bg: #ffffff;
--clndr-accent: #2563eb;
--clndr-danger: #dc2626;
--clndr-radius: 8px;
display: grid;
gap: 1.25rem;
max-width: 36rem;
color: var(--clndr-fg);
font: inherit;
}
@media (prefers-color-scheme: dark) {
.clndr-root {
--clndr-fg: #fafafa;
--clndr-muted: #a1a1aa;
--clndr-border: #3f3f46;
--clndr-bg: #18181b;
}
}
.clndr-heading { margin: 0; font-size: 1.5rem; font-weight: 600; line-height: 1.25; }
.clndr-muted { color: var(--clndr-muted); font-size: 0.875rem; }
.clndr-error { color: var(--clndr-danger); font-size: 0.875rem; }
.clndr-description { font-size: 0.9375rem; line-height: 1.6; }
.clndr-description a { color: var(--clndr-accent); }
.clndr-label { display: block; margin-bottom: 0.375rem; font-size: 0.875rem; font-weight: 500; }
.clndr-input,
.clndr-textarea,
.clndr-date-input {
box-sizing: border-box;
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--clndr-border);
border-radius: var(--clndr-radius);
background: var(--clndr-bg);
color: inherit;
font: inherit;
}
.clndr-textarea { min-height: 6rem; resize: vertical; }
.clndr-input:focus-visible,
.clndr-textarea:focus-visible,
.clndr-date-input:focus-visible,
.clndr-btn:focus-visible,
.clndr-slot-btn:focus-visible {
outline: 2px solid var(--clndr-accent);
outline-offset: 2px;
}
.clndr-slot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
gap: 0.5rem;
margin-top: 1rem;
}
.clndr-slot-btn {
padding: 0.5rem;
border: 1px solid var(--clndr-border);
border-radius: var(--clndr-radius);
background: var(--clndr-bg);
color: inherit;
font: inherit;
cursor: pointer;
}
.clndr-slot-btn:hover { border-color: var(--clndr-accent); color: var(--clndr-accent); }
.clndr-card {
display: grid;
gap: 1rem;
padding: 1.25rem;
border: 1px solid var(--clndr-border);
border-radius: calc(var(--clndr-radius) + 4px);
}
.clndr-btn {
padding: 0.625rem 1rem;
border: 1px solid transparent;
border-radius: var(--clndr-radius);
font: inherit;
font-weight: 600;
cursor: pointer;
}
.clndr-btn-primary { flex: 1; background: var(--clndr-accent); color: #fff; }
.clndr-btn-secondary { background: transparent; border-color: var(--clndr-border); color: inherit; }
.clndr-btn:disabled { opacity: 0.6; cursor: progress; }
.clndr-checkbox { width: 1rem; height: 1rem; accent-color: var(--clndr-accent); }Import it once (import './clndr.css' in a client component, or from your global stylesheet) and render the form with no classNames at all.
The description
Hosts write booking page descriptions in Markdown, and the form renders them: headings, lists, links (opened in a new tab), tables, code. The renderer, react-markdown with remark-gfm, loads in its own chunk the first time a description appears, so pages without one never download it.
Its elements carry shadcn/Tailwind classes (text-primary, bg-muted, border-border, text-muted-foreground), so they pick up a shadcn theme. Two catches:
-
Tailwind only generates classes it finds in your source, and it skips
node_modules. Unless your own code uses the same utilities, tell Tailwind to scan the package. In Tailwind v4, add this to the CSS file that imports Tailwind:app/globals.css @source "../node_modules/@clndr-pro/react/dist";In Tailwind v3, add
'./node_modules/@clndr-pro/react/dist/**/*.{js,mjs}'tocontent. -
Without Tailwind the classes do nothing and the description renders as plain HTML; style it through
.clndr-descriptionlike the stylesheet above.
To use your own renderer (MDX, a prose component you already have), pass MarkdownRenderer. Yours is bundled normally and the built-in chunk is never requested:
'use client';
import Markdown from 'react-markdown';
import { BookingInline } from '@clndr-pro/react';
export function WithOwnMarkdown() {
return (
<BookingInline
slug="intro-call"
components={{
MarkdownRenderer: ({ content, className }) => (
<div className={className}>
<Markdown>{content}</Markdown>
</div>
),
}}
classNames={{ description: 'prose prose-sm dark:prose-invert max-w-none' }}
/>
);
}The prose classes come from @tailwindcss/typography. Without the plugin, use your own typography classes.
Dark mode
The form has no colours of its own, so it follows whatever you give it: shadcn components and tokens switch with your .dark class, and the plain-CSS example switches on prefers-color-scheme (swap that media query for your own dark selector if you have a toggle). The exception is BookingModal's built-in white panel; pass contentClassName and overlayClassName to restyle it, or use your own dialog.