A small, headless-friendly React appointment / time-slot booking widget.
It ships a compound-component API: one AppointmentWidget provider holds the
state (selected date, selected slot, confirm handler) and the visual building
blocks (Calendar, TimeSlotList, SelectedSummary, ConfirmButton) read
from it via context. You can drop the defaults in as-is, restyle them with the
included stylesheet, or replace any piece with your own component powered by
the useAppointment hook.
- Compound components with shared state via React context
- Month-grid calendar with configurable
minDate/maxDate - Built-in slot generator (
startHour,endHour,durationMinutes) or plug in your ownslotsForDateresolver (e.g. fetched from an API) onConfirmcallback returning the selected{ date, slot }- Optional base stylesheet — BYO CSS works too, every element has a stable
classhook - Ships ESM + CJS + generated
.d.tstypes; zero runtime dependencies beyond React 18
npm install react-appointment-widgetPeer dependencies: react >= 18 and react-dom >= 18.
import {
AppointmentWidget,
Calendar,
TimeSlotList,
SelectedSummary,
ConfirmButton,
type ConfirmedAppointment
} from "react-appointment-widget";
import "react-appointment-widget/styles/base.css";
export const BookingPage = () => {
const handleConfirm = ({ date, slot }: ConfirmedAppointment) => {
console.log("Booked", date, slot);
};
return (
<AppointmentWidget onConfirm={handleConfirm}>
<Calendar />
<TimeSlotList />
<SelectedSummary />
<ConfirmButton />
</AppointmentWidget>
);
};The root provider. All child components must be rendered inside it.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
— | Compound children (Calendar, TimeSlotList, etc.). |
onConfirm |
(appt: ConfirmedAppointment) => void |
— | Called when the user confirms a selected date + slot. |
minDate |
Date |
today | Earliest selectable date. |
maxDate |
Date |
today + 45 days | Latest selectable date. |
slotOptions |
{ startHour?; endHour?; durationMinutes? } |
9, 17, 30 |
Forwarded to the default slot generator. |
slotsForDate |
(date: Date) => TimeSlot[] |
— | Custom slot resolver; overrides the default generator when set. |
className |
string |
— | Extra class appended to the root .appointment element. |
<Calendar />— month grid with prev/next navigation, respectsminDate/maxDate.<TimeSlotList />— grid of selectable slots for the current date.<SelectedSummary />— read-only recap of the current selection.<ConfirmButton label?="Confirm appointment" />— disabled until both a date and a slot are chosen; callsonConfirmwhen clicked.
For building your own UI on top of the same state:
import { useAppointment } from "react-appointment-widget";
const CustomPicker = () => {
const {
selectedDate,
selectDate,
selectedSlot,
selectSlot,
slots,
minDate,
maxDate,
canConfirm,
confirm
} = useAppointment();
// render anything you like with this state
};Must be called inside an <AppointmentWidget> — it throws otherwise.
type TimeSlot = {
id: string;
start: string; // "HH:mm"
end: string; // "HH:mm"
isAvailable: boolean;
};
type ConfirmedAppointment = {
date: Date;
slot: TimeSlot;
};Also exported: AppointmentState, AppointmentContextValue, TimeSlotOptions.
Re-exported for convenience:
generateTimeSlots(date, options?),DEFAULT_SLOT_OPTIONSstartOfDay,addDays,startOfMonth,addMonths,isSameDay,formatISODate,parseISODate
<AppointmentWidget
slotsForDate={(date) => mySlotCache.get(formatISODate(date)) ?? []}
onConfirm={submitBooking}
>
<Calendar />
<TimeSlotList />
<ConfirmButton />
</AppointmentWidget><AppointmentWidget
minDate={new Date()}
maxDate={addDays(new Date(), 14)}
slotOptions={{ startHour: 10, endHour: 18, durationMinutes: 45 }}
>
{/* ... */}
</AppointmentWidget>The optional stylesheet is exported at react-appointment-widget/styles/base.css.
Every element exposes a stable BEM-style class so you can override selectively:
.appointment.calendar,.calendar__header,.calendar__nav,.calendar__title,.calendar__grid,.calendar__weekday,.calendar__day,.calendar__day--muted,.calendar__day--selected.slots,.slots__title,.slots__empty,.slots__grid,.slots__slot,.slots__slot--selected,.slots__slot--unavailable.selected-summary,.selected-summary__title,.selected-summary__list,.selected-summary__row.confirm-button
If you don't import the base CSS, the components render unstyled — use the
classes (or the useAppointment hook) to build your own look.
npm install
npm run dev # runs the Vite demo under ./demo
npm run typecheck # tsc --noEmit
npm run build:demo # typecheck + production demo buildThe demo app lives in demo/ and imports the library from src/ directly, so
changes hot-reload end-to-end.
MIT