Skip to content
kerfjsGitHub

BookingCalendar

Pick a day and a free time, confirm, and get a confirmation.

SourcePackage @kerf-js/veneer

Preview

tsx
import { state } from '@kerf-js/core'import {  BookingCalendar,  Stack,  Text} from '@kerf-js/veneer'import type {  BookingDetails,  BookingSlot} from '@kerf-js/veneer'import { at, clock } from '@kerf-js/veneer-core'export function Onboarding() {  const booked = state<BookingDetails | null>(null)  const book = async (b: BookingDetails) => {    await pause(800)    booked.value = b  }  return (    <Stack gap="related">      <BookingCalendar        slots={slotsFor}        defaultDate={TODAY}        min={TODAY}        today={TODAY}        locale="en-GB"        duration={30}        isDateUnavailable={isWeekend}        onBook={book}      />      {booked.value && (        <Text          role="caption"          subtle        >          {'Onboarding call booked for '}          {booked.value.date} at{' '}          {clock(booked.value.s, 'en-GB')}.        </Text>      )}    </Stack>  )}const TODAY = '2026-11-03'const min = 60_000const pause = (ms: number) =>  new Promise((done) => {    setTimeout(done, ms)  })const TIMES = [540, 570, 600, 630, 660, 780, 810, 840]const slotsFor = (date: string): BookingSlot[] =>  TIMES.map((s, i) => ({    s,    disabled: (i + Number(date.slice(8))) % 4 === 0,  }))const isWeekend = (date: string) =>  new Date(date).getUTCDay() % 6 === 0

Installation

npm install @kerf-js/veneer

Usage

tsx
import { BookingCalendar } from '@kerf-js/veneer'

Loading

Set loading to show that times are on their way.

tsx
import { BookingCalendar } from '@kerf-js/veneer'export function Loading() {  return (  <BookingCalendar    slots={[]}    loading    defaultDate={TODAY}    today={TODAY}    locale="en-GB"    onBook={() => {}}  />  )}const TODAY = '2026-11-03'

Times failed to load

Use error to say the times could not be loaded.

tsx
import { BookingCalendar } from '@kerf-js/veneer'export function Failed() {  return (  <BookingCalendar    slots={[]}    error={bookingError}    defaultDate={TODAY}    today={TODAY}    locale="en-GB"    onBook={() => {}}  />  )}const TODAY = '2026-11-03'const bookingError =  "We couldn't load the times. Try again in a moment."

When to use

Use it for

  • Someone books time with you or your team

Reach for something else

Accessibility

  • Each time is a pressed-or-not button. A typed time is always offered as well.

API Reference

PropTypeDefaultDescription
slotsBookingSlot[] | ((date: string) => BookingSlot[])—The times: a list, or a function of the date.
date`${number}-${number}-${number}` | State<`${number}-${number}-${number}` …—The picked day: a state it writes, or a plain value with onDateChange.
defaultDate`${number}-${number}-${number}`—The day picked at the start, when the calendar keeps its own state.
durationnumber—Minutes.
minstring—The lowest allowed value.
maxstring—The highest allowed value.
isDateUnavailable((d: `${number}-${number}-${number}`) => boolean)—Days to close.
loadingbooleanfalseThe times are on their way.
errorunknown—Why the times failed.
todaystring—Pins today's date, for server rendering and tests. The browser's clock otherwise.
localestring—The language and region used to format values.
hourCyclestring—h12 for AM and PM, h23 for a 24-hour clock. The locale's otherwise.
EventTypeDefaultDescription
onDateChange((d: `${number}-${number}-${number}`) => void)—Called with the day picked. Load that day's slots here.
onBook(b: BookingDetails) => void | Promise<unknown>—Confirmed. Rejecting keeps the confirm step and says why.

Parts

Every element carries a stable name, so you can style it from your own CSS: [data-scope="booking"] [data-part="root"]

rootslots

RelatedMeeting plannerInline calendar