ComponentsSlotPlanner
SlotPlanner
Manage available time slots and let users request bookings in their time zone.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry files- Internationalized Date
(@internationalized/date)Date values, calendar arithmetic, and time-zone-aware date handling.
- Motion for React
(motion)Animations, transitions, and reduced-motion handling.
- React Aria
(react-aria)· via shared Dethink componentsAccessibility hooks and overlay utilities used by the implementation.
- React Aria Components
(react-aria-components)· via shared Dethink componentsAccessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
import {
SlotPicker,
SlotPlanner,
useSlotPlanner,
validateSlotPlannerSlots,
type SlotPlannerConstraints,
type SlotPlannerSlotData,
} from "@dethink/components";SlotPlanner manages the provider-facing inventory; SlotPicker projects the same slots for booking.
Manage mode
Compare availability across the week. Select a slot to manage its day; narrow containers show a daily agenda.
America/New York
Tuesday, September 22, 2026
- 09:00 – 09:45Available
2 seats left
45 minRecurring weeklyMentoringAmerica/New_York
- 14:00 – 14:30Request pending
2 seats left
30 minGroup session — drop-in welcome.
America/New_York
- 16:00 – 17:00Booked
0 seats left
60 minMentoringAmerica/New_York
Show sourceexamples/slot-planner/basic.tsx
"use client";
import { SlotPlanner, type SlotPlannerSlotData } from "@dethink/components";
const sampleSlots: SlotPlannerSlotData[] = [
{
id: "mon-morning",
date: "2026-09-22",
startTime: "09:00",
durationMinutes: 45,
timeZone: "America/New_York",
state: "requestable",
capacity: 2,
recurrence: { frequency: "weekly" },
data: { tags: ["Mentoring"] },
},
{
id: "mon-afternoon",
date: "2026-09-22",
startTime: "14:00",
durationMinutes: 30,
timeZone: "America/New_York",
state: "requestable",
capacity: 3,
requestedCount: 1,
data: { note: "Group session — drop-in welcome." },
},
{
id: "tue-standup",
date: "2026-09-21",
startTime: "10:00",
durationMinutes: 45,
timeZone: "America/New_York",
state: "requestable",
data: { tags: ["Mentoring"] },
},
{
id: "wed-review",
date: "2026-09-23",
startTime: "11:00",
durationMinutes: 60,
timeZone: "America/New_York",
state: "requestable",
bookedCount: 1,
capacity: 1,
},
{
id: "tue-booked",
date: "2026-09-22",
startTime: "16:00",
durationMinutes: 60,
timeZone: "America/New_York",
state: "requestable",
capacity: 1,
bookedCount: 1,
data: { tags: ["Mentoring"] },
},
{
id: "thu-available",
date: "2026-09-24",
startTime: "13:00",
durationMinutes: 30,
timeZone: "America/New_York",
state: "requestable",
capacity: 2,
},
];
export function SlotPlannerBasic() {
return (
<SlotPlanner
title="Mentoring availability"
defaultSlots={sampleSlots}
defaultFocusedDate="2026-09-22"
now="2026-09-21T08:00:00-04:00"
timeZone="America/New_York"
taxonomy={{
statusLabels: {
requestable: "Available",
requested: "Request pending",
booked: "Booked",
},
emptyDay: "No slots",
addSlot: "Add slot",
}}
/>
);
}Constraints and cap meter
A daily requestable cap, minimum notice, and a duration increment surfaced inline as the cap fills.
America/New York
Monday, July 6, 2026
Daily cap: 3 / 3 requestable slotsDaily cap reached
- 09:00 – 09:30requested
0 seats left
30 minAmerica/New_York
- 11:00 – 11:30requested
0 seats left
30 minAmerica/New_York
- 15:00 – 15:30requested
0 seats left
30 minAmerica/New_York
Show sourceexamples/slot-planner/constraints.tsx
"use client";
import { SlotPlanner, type SlotPlannerSlotData } from "@dethink/components";
const sampleSlots: SlotPlannerSlotData[] = [
{
id: "mon-early",
date: "2026-07-06",
startTime: "09:00",
durationMinutes: 30,
timeZone: "America/New_York",
state: "requestable",
requestedCount: 1,
},
{
id: "mon-mid",
date: "2026-07-06",
startTime: "11:00",
durationMinutes: 30,
timeZone: "America/New_York",
state: "requestable",
requestedCount: 1,
},
{
id: "mon-late",
date: "2026-07-06",
startTime: "15:00",
durationMinutes: 30,
timeZone: "America/New_York",
state: "requestable",
requestedCount: 1,
},
];
export function SlotPlannerConstraints() {
return (
<SlotPlanner
title="Advising hours"
timeZone="America/New_York"
defaultSlots={sampleSlots}
defaultFocusedDate="2026-07-06"
now="2026-07-06T08:00:00-04:00"
constraints={{
dailyRequestableCap: 3,
minNoticeMinutes: 120,
durationIncrementMinutes: 15,
}}
/>
);
}Custom taxonomy
The same component phrased through a custom vocabulary — "sessions" instead of "slots".
America/Los Angeles
Monday, July 6, 2026
- 13:00 – 13:50requestable
1 seat left
50 minRecurring weeklyCBTAmerica/Los_Angeles
Show sourceexamples/slot-planner/taxonomy.tsx
"use client";
import { SlotPlanner, type SlotPlannerSlotData } from "@dethink/components";
const sampleSlots: SlotPlannerSlotData[] = [
{
id: "mon-session",
date: "2026-07-06",
startTime: "13:00",
durationMinutes: 50,
timeZone: "America/Los_Angeles",
state: "requestable",
recurrence: { frequency: "weekly" },
data: { tags: ["CBT"] },
},
{
id: "wed-session",
date: "2026-07-08",
startTime: "16:00",
durationMinutes: 50,
timeZone: "America/Los_Angeles",
state: "requestable",
data: { note: "Intake session for new clients." },
},
];
export function SlotPlannerTaxonomy() {
return (
<SlotPlanner
title="Therapy sessions"
timeZone="America/Los_Angeles"
defaultSlots={sampleSlots}
defaultFocusedDate="2026-07-06"
now="2026-07-06T09:00:00-07:00"
taxonomy={{
slot: "session",
slotPlural: "sessions",
addSlot: "Add session to this day",
requestSlot: "Request {slot}",
}}
/>
);
}Book mode with a distinct viewer zone
SlotPicker projects provider-zone slots into a different viewer time zone and reports requests without mutating the collection itself.
America/New York
Monday, July 6, 2026
- 09:00 – 09:30requestable
14:00 Europe/London
2 seats left
Show sourceexamples/slot-planner/book-mode.tsx
"use client";
import { useState } from "react";
import {
SlotPicker,
type SlotPlannerBookRequestPayload,
type SlotPlannerSlotData,
} from "@dethink/components";
const initialSlots: SlotPlannerSlotData[] = [
{
id: "mon-consult",
date: "2026-07-06",
startTime: "14:00",
durationMinutes: 30,
timeZone: "Europe/London",
state: "requestable",
recurrence: { frequency: "weekly" },
capacity: 2,
},
{
id: "tue-consult",
date: "2026-07-07",
startTime: "17:30",
durationMinutes: 30,
timeZone: "Europe/London",
state: "requestable",
capacity: 1,
},
];
export function SlotPlannerBookMode() {
const [slots, setSlots] = useState(initialSlots);
const handleBookRequest = (payload: SlotPlannerBookRequestPayload) => {
setSlots((previous) =>
previous.map((slot) => {
if (slot.id !== payload.slotId) {
return slot;
}
if (!slot.recurrence || payload.occurrenceDate === slot.date) {
return { ...slot, requestedCount: (slot.requestedCount ?? 0) + 1 };
}
const overrides = slot.recurrence.overrides ?? [];
const existing = overrides.find(
(override) => override.occurrenceDate === payload.occurrenceDate,
);
const nextOverride = {
...existing,
occurrenceDate: payload.occurrenceDate,
requestedCount: (existing?.requestedCount ?? 0) + 1,
};
return {
...slot,
recurrence: {
...slot.recurrence,
overrides: existing
? overrides.map((override) =>
override.occurrenceDate === payload.occurrenceDate
? nextOverride
: override,
)
: [...overrides, nextOverride],
},
};
}),
);
};
return (
<SlotPicker
title="Book a consultation"
slots={slots}
viewerTimeZone="America/New_York"
defaultFocusedDate="2026-07-06"
now="2026-07-06T08:00:00-04:00"
onBookRequest={handleBookRequest}
/>
);
}Custom slot card renderer
Every surface exposes renderDefault() so a custom renderer can decorate the shipped card instead of rebuilding it.
Monday, July 6, 2026
- 10:00 – 10:45requestable45 minRecurring weekly
America/New_York
$120 per seat
- 15:00 – 16:30requestable90 min
America/New_York
$45 per seat
Show sourceexamples/slot-planner/custom-renderer.tsx
"use client";
import { SlotPlanner, type SlotPlannerSlotData } from "@dethink/components";
const sampleSlots: SlotPlannerSlotData<{ priceUsd: number }>[] = [
{
id: "mon-consult",
date: "2026-07-06",
startTime: "10:00",
durationMinutes: 45,
timeZone: "America/New_York",
state: "requestable",
recurrence: { frequency: "weekly" },
data: { priceUsd: 120 },
},
{
id: "mon-workshop",
date: "2026-07-06",
startTime: "15:00",
durationMinutes: 90,
timeZone: "America/New_York",
state: "requestable",
capacity: 6,
data: { priceUsd: 45 },
},
];
export function SlotPlannerCustomRenderer() {
return (
<SlotPlanner
title="Paid consultations"
timeZone="America/New_York"
defaultSlots={sampleSlots}
defaultFocusedDate="2026-07-06"
now="2026-07-06T08:00:00-04:00"
renderers={{
slotCard: ({ occurrence, renderDefault }) => (
<div className="grid gap-2">
{renderDefault()}
{typeof occurrence.slot.data?.priceUsd === "number" ? (
<p className="text-foreground text-sm font-medium">
${occurrence.slot.data.priceUsd} per seat
</p>
) : null}
</div>
),
}}
/>
);
}Choose a date, then add or manage its slots.
The weekly calendar appears when the component has at least 620px of space. Select Saturday or Sunday to show the weekend. Select an appointment to open its day. The appointment menu contains Edit and Delete; Day actions contains copy and clear options. Choose Week to return to the calendar. Narrow containers show a date strip and daily agenda.
The calendar projects appointments into the time zone shown above it. The day editor uses the original appointment’s zone and date. Pass timeZone explicitly for consistent server and browser rendering. Slots crossing midnight appear on each affected day. Short slots have a minimum hit target and overlaps use separate lanes; the written times give the exact duration.
The editor includes duration presets and a custom duration field. More options contains capacity, buffers, time zone, tags, and notes. The end-time preview updates as you edit. New slots must meet the notice period and start in the future. Existing recurring appointments retain the choice between one occurrence and the whole series.
Use Tab to reach calendar controls and appointments, and Enter or Space to activate them. In the compact date strip, use arrow keys, Home, and End. Dialogs contain focus, close with Escape, and return focus to their trigger. Status labels communicate availability alongside colour. Motion follows reduced-motion preferences.
SlotPicker shows Request sent after its callback succeeds and prevents repeat activation for that occurrence while the picker stays mounted. This acknowledgement is local to the current picker. Your application owns authentication, persistent request state, seat enforcement, cancellation, and duplicate prevention across sessions.
Set weekLayout="agenda" to retain the earlier weekday-rail presentation. Providing custom renderers also retains that presentation, including the existing tab, list-item, and renderer semantics. The slot data model and mutation callback payloads are unchanged.
SlotPlanner (manage mode) and SlotPicker (book mode) share the same slot data model and taxonomy/motion/focus props.
| Prop | What it does | Default |
|---|---|---|
weekLayout"calendar" | "agenda" | Calendar shows a time grid from 620px container width and a daily agenda below it. Agenda retains the original weekday rail. Custom renderers automatically use the agenda to preserve their structural contract. | "calendar" |
slots / defaultSlotsSlotPlannerSlotData[] | Controlled or uncontrolled slot collection. Recurring slots repeat weekly or biweekly with per-occurrence overrides. | uncontrolled |
focusedDate / defaultFocusedDate / onFocusedDateChangestring / string / (dateIso) => void | Controls the focused ISO date (`YYYY-MM-DD`) driving the visible week and selected day. | uncontrolled / today / undefined |
view / defaultView / onViewChange"week" | "day" / "week" | "day" / (view) => void | Controls the visible projection. Day view hides the rail while keeping toolbar navigation and the view switcher. | uncontrolled / "week" / undefined |
timeZonestring | IANA zone used for the weekly calendar, planner today, and new slots. Pass explicitly for deterministic renders and SSR. Existing slot edits retain their original zone. | environment zone |
constraintsSlotPlannerConstraints | Declarative rules — daily/weekly caps, duration bounds, notice period, booking horizon, blackout dates, working days — validated on editor saves and batch operations, and surfaced by the cap meter. | undefined |
taxonomySlotPlannerTaxonomyInput | Overrides any subset of the noun, verb, and announcement vocabulary, e.g. renaming "slot" to "session" or "appointment". | neutral slot language |
onCreateSlot / onUpdateSlot(payload) => void | Promise<void> | Mutation callbacks fired from the editor dialog. A returned promise drives per-key pending/error/retry affordances. | undefined |
onDeleteOccurrence / onDeleteSeries(payload) => void | Promise<void> | Fired for single-occurrence and whole-series deletion, after the structural delete confirm dialog. | undefined |
onBatchChange(payload: SlotPlannerBatchChangePayload) => void | Promise<void> | Fired for copy-day, copy-week, and clear-day operations with created/deleted/updated slots and per-slot violations. | undefined |
now / localestring / string | Injectable "now" instant and locale for deterministic renders, including SSR. | current time / environment locale |
loading / errorboolean / ReactNode | External async states for app-owned fetching. The day panel shows status text and hides mutation affordances while active. | false / undefined |
titleReactNode | Heading rendered above the toolbar. | undefined |
reducedMotionboolean | Forces the reduced-motion rendering path; every animation collapses to an instant state change either way. | prefers-reduced-motion |
renderersSlotPlannerRenderers | Render props for the toolbar, day card, day header, cap meter, empty day, and slot card, each with a renderDefault() escape hatch for decoration. | undefined |
| Prop | What it does | Default |
|---|---|---|
slotsSlotPlannerSlotData[] | Read-only slot collection to browse. SlotPicker never mutates it — a request only fires onBookRequest. | required |
viewerTimeZonestring | IANA zone the occurrences are projected into for display and day grouping. Pass explicitly for deterministic renders and SSR. | environment zone |
onBookRequest(payload: SlotPlannerBookRequestPayload) => void | Promise<void> | Fires when the viewer requests an available occurrence. occurrenceDate stays the provider-zone date; a returned promise drives pending/error/retry. | undefined |
taxonomySlotPlannerTaxonomyInput | Same vocabulary override as SlotPlanner, phrased for book mode. | neutral slot language |
view / defaultView / onViewChange"week" | "day" / "week" | "day" / (view) => void | Controls the visible projection. Day view hides the rail while keeping toolbar navigation and the view switcher. | uncontrolled / "week" / undefined |
focusedDate / defaultFocusedDate / onFocusedDateChangestring / string / (dateIso) => void | Controls the focused viewer-zone ISO date driving the visible week and selected day. | uncontrolled / viewer-zone today / undefined |
now / localestring / string | Injectable "now" instant and locale for deterministic renders. | current time / environment locale |
loading / errorboolean / ReactNode | External async states for app-owned fetching. The day panel shows status text and hides the slot list while active. | false / undefined |
titleReactNode | Heading rendered above the toolbar. | undefined |
reducedMotionboolean | Forces the reduced-motion rendering path. | prefers-reduced-motion |
renderersSlotPickerRenderers | Render props for the slot card and empty day, each with a renderDefault() escape hatch for decoration. | undefined |