DateForgeDocumentation
React calendarModular · Composable · Tokenized

Build exactly the calendar your product needs.

DateForge gives you a stateful calendar shell and a set of small modules: days, navigation, tracks, time, presets, selected chips, and custom context hooks. Start with one picker, then grow into the composition your workflow needs.

$npm i @dateforge/react-calendar

Quick start

tsx
import { useState } from "react"; import { Calendar, createCalendarConfig } from "@dateforge/react-calendar"; import { CalendarDays } from "@dateforge/react-calendar/modules"; import { CalendarToolbar, CalendarToolbarPrev, CalendarToolbarMonthTrigger, CalendarToolbarNext, CalendarToolbarYearTrigger, } from "@dateforge/react-calendar/modules/toolbar"; // Compiled once at module scope — unit "day", mode "single" by default. const config = createCalendarConfig(); export function DatePicker() { const [date, setDate] = useState<Date | null>(null); return ( <Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)} > <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> </Calendar> ); }

No global CSS import is required. Every module ships its own styles and applies them on first render. In RSC frameworks, render the calendar behind a "use client" boundary.

Core idea

DateForge is a stateful composition wrapper with self-contained modules. Behavior is compiled once with createCalendarConfig({ mode, unit, locale, min, max, disabled, … }) and passed to the <Calendar> shell as a single config prop. The shell owns the value, view date, theme, appearance, scheme, and onChange wiring. It renders no picker UI by itself.

Visible behavior comes from modules placed as children: CalendarToolbar, CalendarDays, CalendarTimeWheel, CalendarPresets, CalendarSelectedDates, manual input, and track modules. You mount only the UI your product needs.

The wrapper also provides a small grid contract: cols defines equal parent tracks, and child col={number} spans that many tracks. For example, inside cols={4}, a module with col={2} takes half the row. col is a span count, not CSS grid-line syntax.

Since 3.1, a numeric cols={N} is responsive: it renders up to N equal columns and collapses toward one on narrow screens. The collapse threshold is the --cal-cols-min CSS variable (default 14em) — set it to 0px for fixed, never-collapsing tracks. col="full" places a module across the entire row, collapse-safely. String cols values stay raw grid-template-columns and never collapse.

Calendar core architecture

When not to use DateForge

DateForge is a picker composition kit, not a full calendar application or a date utility library. It is strongest when your product needs a custom date, range, or date-time picker built from small React modules.

Choose something simpler or more specialized when:

  • You only need a native browser field like <input type="date">, <input type="time">, or <input type="datetime-local">.
  • You want one prebuilt picker with almost no composition decisions, styling decisions, or module choices.
  • You need event-calendar features: drag-and-drop events, resource columns, agenda views, recurring events, ICS import/export, or meeting scheduling logic.
  • You need general date math, timezone conversion, parsing, or formatting utilities. Pair DateForge with Temporal, date-fns, dayjs, or your app's existing date layer for that work.
  • You need a non-React widget, a server-rendered-only calendar, or a framework-agnostic web component.
  • You need a full form field abstraction with labels, validation messages, popovers, input masks, and form-library bindings already bundled.

Modules

Modules read calendar context directly, so there is no prop drilling. They can be reordered, repeated, or used alone. Any subset should render without crashing, but not every subset is a complete human-friendly UX.

Module groupModulesPrimary role
NavigationCalendarToolbar, CalendarMonthsGrid, CalendarYearsGridMove the internal viewDate without committing selection
SelectionCalendarDays, CalendarTimeWheel, CalendarManualInput, CalendarPresetsCommit dates, ranges, arrays, or time changes
FeedbackCalendarSelectedDates, CalendarInfoRender current selection as chips, summary, or info readout
TracksCalendarDaysTrack, CalendarMonthsTrack, CalendarYearsTrackHorizontal scrollable strips for compact / mobile layouts
WheelsCalendarMonthsWheel, CalendarYearsWheeliOS-style drum pickers for month and year, range-bound aware
DecorativeCalendarLunarLunar phase strip around selected date (display-only)
CustomStore hooks from @dateforge/react-calendar/context (useCalendarStore, useStoreSelector, useCalendarActions, useUI, useLabels)Build your own modules on top of the same store

Selection model: unit × mode

Two config axes decide the value shape and selection semantics: unit ("day" | "week" | "month", default "day") and mode ("single" | "multiple" | "range" | "multi-range", default "single"). The onChange shape is fully determined by the pair:

unit × modeValue / defaultValueCleared valueBest for
day + singleDate | nullnullDate picker, scheduler date, time-only picker
day + multipleDate[] (sorted)[]Delivery days, shifts, events
any single span (range, or week/month single){ start: Date; end: Date } | nullnullBooking, reporting windows, week/month pickers
any multi span (multi-range, week/month multiple){ start: Date; end: Date }[][]Blackout windows, multi-sprint planning

Spans are always emitted complete — while the user is still drawing a range, the value stays null (or keeps the previous committed span). onChange also receives a second details argument (CalendarChangeDetails) with reason and segments — the business-day cut when exclude/disabled rules apply. Caps and lengths live in the config too: minSpan/maxSpan (in units), maxDates for multiple, maxRanges for multi-range.

Which modules do I need?

Start from the product workflow, then pick modules. The calendar does not force one canonical picker.

You want...Compose these modules
Basic date pickerCalendarToolbar + CalendarDays
Range pickerCalendarToolbar + CalendarDays with config = createCalendarConfig({ mode: "range" })
Date and time pickerCalendarToolbar with CalendarToolbarTime + CalendarDays, or inline CalendarTimeWheel
Time-only pickerCalendarTimeWheel with createCalendarConfig({ withTime: true })
Month/year drum pickersCalendarMonthsWheel + CalendarYearsWheel
Manual typingCalendarManualInput, optionally with CalendarDays
Preset shortcutsCalendarPresets alongside any picker modules
Month-only or year-only pickerCalendarMonthsGrid or CalendarYearsGrid without CalendarDays
Mobile / compact stripsCalendarDaysTrack, CalendarMonthsTrack, CalendarYearsTrack
Selection summaryCalendarSelectedDates
Date facts, range duration, relative timeCalendarInfo
Lunar phase displayCalendarLunar (decorative, no interaction)

Import strategy

The package is split into tree-shakeable subpaths. The aggregate paths are convenient for prototyping; per-subpath imports keep production bundles small.

Import fromWhat is thereWhen to use
@dateforge/react-calendarCalendar, createCalendarConfig, factories (createTheme, createAppearance, createDisabled, definePreset), preset packs, public typesAlways; root provider lives here
@dateforge/react-calendar/prebuiltSimpleCalendar, DatePicker, MonthPicker, MultiMonthCalendarOne-import calendars, zero composition
@dateforge/react-calendar/modulesAll Calendar* modules (days, tracks, wheels, info, etc.)Prototyping; grab all at once
@dateforge/react-calendar/modules/<name>One module - e.g. modules/days, modules/time, modules/lunarProduction bundle hygiene
@dateforge/react-calendar/modules/toolbarAll CalendarToolbar* sub-componentsToolbar composition
@dateforge/react-calendar/contextStore hooks: useCalendarStore, useStoreSelector, useCalendarActions, useUI, useLabelsBuilding custom modules
@dateforge/react-calendar/themesAll 28 theme families (named exports + THEMES)Theme objects; tree-shaken per import
@dateforge/react-calendar/appearancesAll 8 appearance objects (+ createAppearance, token sets)Appearance objects; tree-shaken per import

Built-in themes and appearances can also be referenced by plain string name (theme="dracula", appearance="zenith") — the generated stylesheet resolves them at runtime. Strings keep every palette reachable; imported objects tree-shake to just the ones you use.

Toolbar imports — all toolbar primitives ship together on one subpath (the module is one bundle; unused primitives tree-shake out):

tsx
import { CalendarToolbar, CalendarToolbarPrev, CalendarToolbarMonthTrigger, CalendarToolbarYearTrigger, CalendarToolbarNext, CalendarToolbarClear, CalendarToolbarHome, CalendarToolbarThemeToggle, CalendarToolbarTime, CalendarToolbarLabel, } from "@dateforge/react-calendar/modules/toolbar";

Module imports - two patterns:

tsx
// Aggregate import { CalendarDays, CalendarTimeWheel, CalendarLunar } from "@dateforge/react-calendar/modules"; // Per-module import { CalendarDays } from "@dateforge/react-calendar/modules/days"; import { CalendarTimeWheel } from "@dateforge/react-calendar/modules/time"; import { CalendarLunar } from "@dateforge/react-calendar/modules/lunar"; import { CalendarMonthsWheel } from "@dateforge/react-calendar/modules/months-wheel"; import { CalendarYearsWheel } from "@dateforge/react-calendar/modules/years-wheel";

Performance with multiple calendars

Three or more visible calendars are reasonable, but treat them as a layout and state-design decision. Most slowdowns come from mounting more modules than the screen needs, recreating config objects on every parent render, or rendering several independent providers when one shared calendar state would do.

For a year-style picker, prefer one <Calendar> with multiple offset nav/day pairs instead of twelve separate calendars:

Open
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
tsx
const config = createCalendarConfig({ mode: "range" }); <Calendar config={config} value={range} onChange={handleChange} cols={3} appearance={compact}> {/* offset 0 — only calendar with prev/next and year trigger */} <CalendarToolbar col={1} offset={0}> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> {/* offsets 1–11 — month + year labels only, no arrows */} <CalendarToolbar col={1} offset={1}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarToolbar col={1} offset={2}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarDays col={1} showOutsideDays={false} fixedWeeks={false} /> <CalendarDays col={1} offset={1} showOutsideDays={false} fixedWeeks={false} /> <CalendarDays col={1} offset={2} showOutsideDays={false} fixedWeeks={false} /> <CalendarToolbar col={1} offset={3}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarToolbar col={1} offset={4}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarToolbar col={1} offset={5}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarDays col={1} offset={3} showOutsideDays={false} fixedWeeks={false} /> <CalendarDays col={1} offset={4} showOutsideDays={false} fixedWeeks={false} /> <CalendarDays col={1} offset={5} showOutsideDays={false} fixedWeeks={false} /> <CalendarToolbar col={1} offset={6}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarToolbar col={1} offset={7}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarToolbar col={1} offset={8}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarDays col={1} offset={6} showOutsideDays={false} fixedWeeks={false} /> <CalendarDays col={1} offset={7} showOutsideDays={false} fixedWeeks={false} /> <CalendarDays col={1} offset={8} showOutsideDays={false} fixedWeeks={false} /> <CalendarToolbar col={1} offset={9}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarToolbar col={1} offset={10}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarToolbar col={1} offset={11}><CalendarToolbarMonthLabel /><CalendarToolbarYearLabel /></CalendarToolbar> <CalendarDays col={1} offset={9} showOutsideDays={false} fixedWeeks={false} /> <CalendarDays col={1} offset={10} showOutsideDays={false} fixedWeeks={false} /> <CalendarDays col={1} offset={11} showOutsideDays={false} fixedWeeks={false} /> <CalendarSelectedDates col={3} /> </Calendar>

Keep expensive props stable when the parent rerenders:

  • Compile the config once: useMemo(() => createCalendarConfig({...}), [...]) or module scope. A new config object on every render forces every module to re-read state.
  • Keep custom presets, theme, and appearance objects outside render or behind useMemo.
  • Mount only the modules the current surface needs; avoid three CalendarTimeWheel or track stacks unless all are visible and interactive.
  • Import theme/appearance objects from the barrels for production bundles; string names keep the full generated stylesheet reachable.
  • In long forms, consider mounting the picker only when its popover, tab, or step is active.

Measure in production mode on the target device class. Dev mode and React Strict Mode exaggerate render work.

tsx
import { Profiler } from "react"; function onCalendarRender( id: string, phase: "mount" | "update" | "nested-update", actualDuration: number, baseDuration: number, ) { console.table({ id, phase, actualDuration: `${actualDuration.toFixed(1)}ms`, baseDuration: `${baseDuration.toFixed(1)}ms`, }); } <Profiler id="booking-calendar" onRender={onCalendarRender}> <BookingCalendar /> </Profiler>

Useful checks:

  • React DevTools Profiler: record initial mount, next/previous month, day selection, range hover, and preset click.
  • Chrome Performance panel: throttle CPU, record the same interactions, and watch scripting time plus input delay.
  • Browser Performance API: wrap product-specific work inside performance.mark() and performance.measure() around handlers that react to onChange.
  • Bundle inspection: compare aggregate imports against per-theme and per-appearance subpaths when bundle size matters.
  • Real user metrics: watch INP and long tasks on pages where calendars are visible by default.

When does each action fire onChange?

The rule of thumb is simple: navigation changes the view, selection commits values.

ActionChanges viewDateChanges selectionFires onChange
Toolbar
CalendarToolbar prev / next / homeyesnono
CalendarToolbarMonthTrigger / CalendarToolbarYearTrigger popupyesnono
Day grid
CalendarDays day clickif cross-monthyesyes
CalendarDays keyboard navigationif cross-monthnono
CalendarTimeWheel drum scrollyesyesyes
Tracks (CalendarDaysTrack, CalendarMonthsTrack, CalendarYearsTrack)
Track scroll without boundyesnono
Track scroll with boundyesyesyes
Wheels (CalendarMonthsWheel, CalendarYearsWheel)
Wheel spin without boundyesnono
Wheel spin with boundyesyesyes
Other
CalendarPresets clickyesyesyes
CalendarSelectedDates chip clickyesnono
CalendarSelectedDates per-chip removenoyesyes
Clear buttonsnoyesyes

readOnly blocks every selection-affecting action, but navigation stays enabled. UI-only state like popup open/close and theme toggles does not fire onChange.

Controlled and uncontrolled

Controlled mode starts when value is provided, including null. User actions fire onChange with the next value, but rendered selection stays tied to the value you pass back.

tsx
const config = createCalendarConfig(); const [date, setDate] = useState<Date | null>(null); <Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> </Calendar>

Uncontrolled mode starts when value is undefined. defaultValue seeds the reducer once on mount, internal state owns future changes, and onChange still fires.

tsx
<Calendar config={config} defaultValue={new Date()} onChange={(date) => console.log(date)}> <CalendarDays /> </Calendar>

When both value and defaultValue are passed, value wins. If you swap in a config with a different unit/mode at runtime, pass a compatible value at the same time; selection shape is not migrated for you.

Accessibility labels

DateForge ships English aria-label defaults for icon buttons, toolbars, dialogs, spinbuttons, tracks, and overflow controls. In v3 they live in one labels registry instead of dozens of per-component props. Resolution is tiered: module prop → root labels → English default.

Pass a partial registry to <Calendar labels={...}> to localize globally; a handful of modules keep small per-instance overrides (e.g. clearLabel on CalendarSelectedDates, weekLabel on CalendarDays).

tsx
<Calendar config={config} labels={{ clear: "Buchung löschen", previousMonth: "Vorheriger Monat", nextMonth: "Nächster Monat", selectMonth: "Monat wählen", selectYear: "Jahr wählen", announceSelected: "{date} ausgewählt", }} > <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays weekLabel="ISO-Woche" /> <CalendarSelectedDates allowClear removeDateLabel="Datum entfernen" /> </Calendar>

Templated labels keep their placeholder names — DateForge interpolates {month}, {year}, {time}, {period}, {count}, {date}, {from}, and {to} where the module has that value available.

Registry keys by area:

AreaKeys
Core actionsclear, apply, confirm, home, remove
NavigationpreviousDay, previousMonth, previousYear, previousYears, nextDay, nextMonth, nextYear, nextYears, calendarNavigation
Timehours, minutes, seconds, selectTime, changeTime, timePeriod, resetTime, timePicker
MonthsselectMonth, changeMonth, currentMonth, monthSelected, monthGrid, monthPicker, resetMonth, monthTrack
YearsselectYear, changeYear, currentYear, yearSelected, yearGrid, yearPicker, resetYear, yearTrack, yearPageNavigation
SelectionmanualInput, rangeFrom, rangeTo, noDate, removeSelectedDate, saveSelectedDate, removeRangeStart, removeRangeEnd, showMoreSelectedDates
Announcements & miscannounceSelected, announceCleared, themeToggle, themeSwitchToLight, themeSwitchToDark, currentDay, dayTrack, week, lunar, infoRanges

The exact LabelOverrides type ships with the package — your editor autocompletes every key.

Ready-made module sets

These are starting points rather than exported presets. Copy the shape, then add constraints, disabled rules, themes, or appearances.

Minimal single date

Open
SunMonTueWedThuFriSat
tsx
const config = createCalendarConfig(); <Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> </Calendar>

Booking range

Open
SunMonTueWedThuFriSat
tsx
const config = createCalendarConfig({ mode: "range" }); // value: { start: Date; end: Date } | null <Calendar config={config} value={range} onChange={handleChange}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> <CalendarToolbarClear /> </CalendarToolbar> <CalendarDays /> <CalendarSelectedDates allowClear allowNavigate /> </Calendar>

Analytics range with presets

Open
SunMonTueWedThuFriSat
tsx
import { type PresetInput } from "@dateforge/react-calendar"; const config = createCalendarConfig({ mode: "range" }); const analyticsPresets: PresetInput[] = [ { label: "Last 7 days", value: -6, range: 6 }, { label: "Last 30 days", value: -29, range: 29 }, { label: "Next sprint", value: 0, range: 13 }, ]; <Calendar config={config} value={range} onChange={handleChange}> <CalendarPresets presets={analyticsPresets} /> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> <CalendarSelectedDates /> </Calendar>

Date and time

Open
SunMonTueWedThuFriSat
10
30
tsx
const config = createCalendarConfig({ withTime: true, defaultTime: { hour: 10, minute: 30 }, }); <Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> <CalendarToolbarTime /> </CalendarToolbar> <CalendarDays /> <CalendarTimeWheel /> </Calendar>

Mobile tracks

Open
2026
May
8
20
tsx
const config = createCalendarConfig({ mode: "range" }); <Calendar config={config} value={range} onChange={handleChange}> <CalendarYearsTrack /> <CalendarMonthsTrack /> <CalendarDaysTrack bound="from" /> <CalendarDaysTrack bound="to" /> <CalendarSelectedDates /> </Calendar>

Prebuilt components

Skip composition entirely: @dateforge/react-calendar/prebuilt ships four ready recipes over the same primitives, with plain-Date props (value, defaultValue, onChange, locale, min/max, disabled, readOnly, theme, appearance, gradient, scheme, and a config escape hatch for extra createCalendarConfig options).

tsx
import { useState } from "react"; import { SimpleCalendar, DatePicker, MonthPicker, MultiMonthCalendar, } from "@dateforge/react-calendar/prebuilt"; const [date, setDate] = useState<Date | null>(null); // Plain-Date props — no config, no composition <SimpleCalendar value={date} onChange={setDate} /> // header + day grid <DatePicker onChange={setDate} /> // typed input + grid + Today jump <MonthPicker onChange={setMonth} /> // year stepper + 12-month grid <MultiMonthCalendar months={6} cols={3} mode="range" /> // 6-month range board

SimpleCalendar

The default calendar: month/year navigation header + day grid, single-date selection. One import and done.

Open
SunMonTueWedThuFriSat
tsx
import { useState } from "react"; import { SimpleCalendar } from "@dateforge/react-calendar/prebuilt"; const [date, setDate] = useState<Date | null>(null); // The default calendar: month/year navigation header + day grid. <SimpleCalendar value={date} onChange={setDate} /> // Same shared props everywhere: locale, min/max, disabled, readOnly, // theme, appearance, gradient, scheme, and a config escape hatch. <SimpleCalendar defaultValue={new Date()} locale="de-DE" min={new Date()} theme="noir" appearance="zenith" />

DatePicker

Typed, segment-based manual input above the calendar, plus a Today jump — keyboard-first single-date entry with the grid as fallback. allowClear (default true) controls the clear button inside the manual input — pass false to hide it.

Open
SunMonTueWedThuFriSat
tsx
import { useState } from "react"; import { DatePicker } from "@dateforge/react-calendar/prebuilt"; const [date, setDate] = useState<Date | null>(null); // Typed, segment-based input above the grid, plus a Today jump — // keyboard-first entry with the grid as fallback. <DatePicker value={date} onChange={setDate} disabled={{ weekends: true }} />

MonthPicker

Year-stepping header + 12-month grid; picking a month selects the whole month (unit: "month" under the hood), reported as its first day.

Open
tsx
import { useState } from "react"; import { MonthPicker } from "@dateforge/react-calendar/prebuilt"; const [month, setMonth] = useState<Date | null>(null); // Year-stepping header + 12-month grid (unit: "month" under the hood). // Picking a month selects the whole month, reported as its first day. <MonthPicker value={month} onChange={setMonth} />

MultiMonthCalendar

3, 6, or 12 consecutive months in a grid, generated on the fly. One shared selection spans the whole board — ranges drag across months. Prev/next arrows on the first and last header step the whole board.

SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
SunMonTueWedThuFriSat
tsx
import { MultiMonthCalendar } from "@dateforge/react-calendar/prebuilt"; // 3/6/12 consecutive months in a grid, generated on the fly. // One shared selection spans the whole board — ranges drag across months. <MultiMonthCalendar months={6} cols={3} mode="range" startMonth={new Date(2026, 6, 1)} onChange={(range, details) => console.log(range, details.reason)} />

When a prebuilt stops fitting, drop one level down to the modules — every prebuilt is just Calendar + modules + createCalendarConfig inside.

Week and month selection

unit switches what one click selects. unit: "week" selects whole weeks, unit: "month" whole months — both emit { start, end } spans:

tsx
const config = createCalendarConfig({ unit: "week" }); // One click selects the whole week. // value: { start: Date; end: Date } | null <Calendar config={config} value={week} onChange={(value) => setWeek(value as { start: Date; end: Date } | null)}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays weekNumbers /> </Calendar>

Multiple ranges

mode: "multi-range" collects several { start, end } spans, capped by maxRanges. CalendarSelectedDates renders one chip pair per span with per-chip removal:

tsx
const config = createCalendarConfig({ mode: "multi-range", maxRanges: 3 }); // value: { start: Date; end: Date }[] <Calendar config={config} value={ranges} onChange={(value) => setRanges(value as { start: Date; end: Date }[])}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> <CalendarSelectedDates allowClear allowClearPerChip /> </Calendar>

Business days: exclude

exclude keeps days spannable but cuts them from the emitted value — the classic "10 business days" flow. onChange details report the resulting segments:

tsx
const config = createCalendarConfig({ mode: "range", exclude: { weekends: true }, // cut from emitted spans, still spannable excludedEndpointPolicy: "snap-inward", // or "reject" }); <Calendar config={config} value={range} onChange={(value, details) => { setRange(value as { start: Date; end: Date } | null); // value = the logical span the user drew; // details.segments = the surviving business-day segments console.log(details.segments); }} > <CalendarDays /> <CalendarSelectedDates allowClear /> </Calendar>

Time, time zones, and DST

withTime adds a time of day to selected values; minTime/maxTime clamp the selectable window in the core (wheels, toolbar time, and manual input all respect it):

tsx
const config = createCalendarConfig({ withTime: true, defaultTime: { hour: 9 }, // applied to a freshly picked day minTime: { hour: 9 }, // inclusive wall-clock floor for every day maxTime: { hour: 18 }, // inclusive ceiling — drums and steppers gate to it }); <Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarDays /> <CalendarTimeWheel labels="short" /> </Calendar>

timeZone pins "today" resolution to an IANA zone, DST-safe, with explicit policies for ambiguous and nonexistent wall-clock times:

tsx
const config = createCalendarConfig({ timeZone: "Asia/Tokyo", // IANA zone — "today" resolves in Tokyo, not the browser zone }); // The today dot, CalendarToolbarHome, and presetToday all agree on // what "today" means, even across the date line. <Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarDays /> </Calendar>

Module reference

Calendar

The root wrapper and context provider. Owns all shared state - mode, value, view date, locale, timezone, theme, appearance, disabled rules, and range constraints - and distributes it to every child module via context. Renders no UI of its own; all visible output comes from the modules you place inside it.

Open
SunMonTueWedThuFriSat
tsx
import { Calendar, createCalendarConfig } from "@dateforge/react-calendar"; // Behavior lives in the config — compiled once, shared by every module. const config = createCalendarConfig({ mode: "single", // "single" | "multiple" | "range" | "multi-range" unit: "day", // "day" | "week" | "month" locale: "en-US", // BCP 47 — month names, weekday labels, week start min: new Date(), // nothing before today is selectable disabled: { weekends: true }, }); <Calendar config={config} value={date} onChange={(value, details) => setDate(value as Date | null)} theme="dracula" // built-in name, or an imported ThemeFamily object appearance={soft} // built-in name, imported object, or createAppearance() scheme="auto" // "auto" | "light" | "dark" > <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> <CalendarSelectedDates allowClear allowNavigate /> </Calendar>

createCalendarConfig

Everything behavioral lives in the config — compiled once, shared by every module:

OptionTypeDefaultDescription
mode"single" | "multiple" | "range" | "multi-range""single"Selection mode
unit"day" | "week" | "month""day"Selection unit — week/month selection picks whole spans
localestringenvBCP-47 locale for names, digits, and week start (via Intl)
firstDayOfWeek0–6from localeWeek start override (0 = Sunday)
min / maxDate-Earliest / latest selectable day (inclusive)
disabledDateRuleConfig | engine-Days that cannot be picked
excludeDateRuleConfig | engine-Days cut out of emitted spans (business-day flows); segments reported in details
excludedEndpointPolicy"snap-inward" | "reject""snap-inward"What happens when a span endpoint lands on an excluded day
readOnlybooleanfalseBlock all selection; navigation stays active
deselectOnReclickbooleantrueClicking the selected day deselects it
withTimebooleanfalseSelected values carry a time of day
hour12 / ampmLabelsboolean / { am, pm }24h12-hour clock and localized AM/PM labels
defaultTime{ hour?, minute?, second? }-Time applied to a freshly picked day
minTime / maxTime{ hour?, minute?, second? }-Inclusive selectable time-of-day window, enforced by the core
weekendDaysnumber[]Sat/SunWhich columns count as weekend
minSpan / maxSpannumber-Span length limits in units (range modes)
maxDatesnumber-Cap on point selections (multiple)
maxRangesnumber-Cap on spans (multi-range)
timeZonestringenvIANA zone for "today" resolution, DST-safe

CalendarToolbar

Composable navigation bar. Place sub-components as children to compose exactly the toolbar your product needs. Controls the internal viewDate - does not commit selection.

The default layout (no cols) is a wrapping flex row: overflow wraps to the next line instead of escaping the container, rows distribute space-between, and an over-wide label shrinks with an ellipsis. Passing cols switches to the explicit grid mode, which never wraps.

tsx
import { CalendarToolbar, CalendarToolbarPrev, CalendarToolbarMonthTrigger, CalendarToolbarYearTrigger, CalendarToolbarNext, } from "@dateforge/react-calendar/modules/toolbar"; <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar>
Open
tsx
<Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> <CalendarToolbarHome /> <CalendarToolbarClear /> </CalendarToolbar> </Calendar>

Sub-components (all imported from @dateforge/react-calendar/modules/toolbar):

ComponentDescription
CalendarToolbarPrevNavigate to previous month/year
CalendarToolbarNextNavigate to next month/year
CalendarToolbarMonthTriggerClickable month label with picker popup. compact for drum picker
CalendarToolbarYearTriggerClickable year label with picker popup. compact for drum picker
CalendarToolbarMonthLabelRead-only month display (no popup)
CalendarToolbarYearLabelRead-only year display (no popup)
CalendarToolbarTimeTime picker trigger (popup with hour/minute drums)
CalendarToolbarClearClear selection button
CalendarToolbarHomeReset to current month button
CalendarToolbarThemeToggleDark/light mode toggle
CalendarToolbarClockLive clock display (ticks every second; isolated re-render)
CalendarToolbarDayLabelRead-only current day-of-week label
CalendarToolbarLabelStatic text label
CalendarToolbarGroupFlex group wrapper - grow fills remaining space; push="start" | "end" pins the group to a toolbar edge in the wrapping flex layout (in cols grid mode the auto margin applies within the group's own cell — prefer col placement there)

CalendarDays

The main day grid. Renders a month view and commits selection on click. Works across all three modes - single, multiple, and range - adapting highlight and click semantics automatically.

Open
WeekSunMonTueWedThuFriSat
17
18
19
20
21
22
tsx
<Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarDays highlightWeekends weekNumbers todayDot /> </Calendar>

CalendarTimeWheel

Drum-scroll time picker for hours, minutes, and optionally seconds. Pairs with CalendarDays for a full date-time picker, or stands alone as a time-only input with createCalendarConfig({ withTime: true }).

Open
10
30
00
tsx
const config = createCalendarConfig({ withTime: true }); <Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarTimeWheel seconds labels="long" step={{ minute: 5 }} /> </Calendar>

CalendarPresets

Shortcut buttons that jump to predefined dates or ranges with a single click. Presets are explicit: pass your own array, commonPresets, relativePresets, or individual exports like presetToday / presetLast7Days. If presets is omitted or empty, the module renders no buttons.

Open
tsx
import { Calendar, commonPresets, createCalendarConfig } from "@dateforge/react-calendar"; import { CalendarPresets } from "@dateforge/react-calendar/modules"; const config = createCalendarConfig({ mode: "range" }); <Calendar config={config} value={range} onChange={handleChange}> <CalendarPresets presets={commonPresets} /> </Calendar>

See custom presets - simple and advanced definitions →

CalendarSelectedDates

Renders the current selection as chips. In range mode shows from/to bounds; in multiple mode shows one chip per date. Chips can navigate to their date or clear individual entries.

Open
tsx
<Calendar config={rangeConfig} value={range} onChange={handleChange}> <CalendarSelectedDates allowClear allowNavigate showTime /> </Calendar>

CalendarManualInput

Free-text date input that parses typed values and syncs them with calendar state. Useful when users know the exact date and prefer typing over clicking.

tsx
<Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarManualInput allowClear /> </Calendar>

CalendarInfo

Read-only summary of the current selection. In single mode prints the date; in multiple prints a count and list; in range prints the bounds plus a duration or day count. Use to surface "facts about the value" - relative time, range length, ISO summary - without rebuilding selection chips. Pass a formatter for fully custom output.

Open
SunMonTueWedThuFriSat
12 days
tsx
<Calendar config={rangeConfig} value={range} onChange={handleChange}> <CalendarDays /> <CalendarInfo showRelative showSummary rangeStyle="duration" /> </Calendar>

CalendarDaysTrack

Horizontal drum scroller for day selection. Designed for mobile-first layouts. Use bound to tie each drum to the from or to side of a range independently.

Open
8
20
tsx
<Calendar config={rangeConfig} value={range} onChange={handleChange}> <CalendarDaysTrack bound="from" showMonthLabel /> <CalendarDaysTrack bound="to" showMonthLabel /> </Calendar>

CalendarMonthsTrack

Drum scroller for month navigation. Scrolling moves the internal viewDate without committing selection. Combine with CalendarDaysTrack for a full mobile drum picker.

Open
May2026
tsx
<Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarMonthsTrack short showYearLabel /> </Calendar>

CalendarYearsTrack

Drum scroller for year navigation. Works the same way as CalendarMonthsTrack but scrolls through years. Stack all three track modules for a compact iOS-style date picker.

Open
2026
tsx
<Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarYearsTrack /> </Calendar>

CalendarMonthsGrid

12-cell month grid for month-only pickers or fast month navigation. Clicking a cell moves viewDate to that month. Use onMonthSelect to build a standalone month picker without CalendarDays.

Open
tsx
<Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarMonthsGrid short /> </Calendar>

CalendarYearsGrid

Paginated year grid for year-only pickers or quick year jumps. Pairs with CalendarMonthsGrid to build a full month-year selector without the day view.

Open
2016–2027
tsx
<Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarYearsGrid showControls yearsPerPage={12} /> </Calendar>

CalendarMonthsWheel

iOS-style drum picker for months. Range-bound aware - pass bound="from" or bound="to" to edit one boundary independently.

Open
May
tsx
<Calendar config={rangeConfig} value={range} onChange={handleChange}> <CalendarMonthsWheel showLabel showReset /> </Calendar>

CalendarYearsWheel

iOS-style drum picker for years. Range-bound aware.

Open
2026
tsx
<Calendar config={rangeConfig} value={range} onChange={handleChange}> <CalendarYearsWheel showLabel showReset /> </Calendar>

CalendarLunar

Informational lunar phase strip centered around the selected date. Display-only - no interaction, no onChange.

Open
SunMonTueWedThuFriSat
tsx
<Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> <CalendarLunar /> </Calendar>

Custom day rendering

CalendarDays accepts a renderDay prop to replace the contents of each day cell with your own JSX - weather icons, price tags, activity heatmaps, event dots, anything. Selection, hover, keyboard navigation, range painting, and disabled handling stay owned by the module; you only swap the visual content inside the cell.

tsx
import { CalendarDays, type DayRenderState } from "@dateforge/react-calendar/modules/days"; type RenderDay = (date: CalendarDate, state: DayRenderState) => React.ReactNode;

The callback receives the cell's CalendarDate ({ year, month, day }, month 1-12) and a DayRenderState flag bag:

FlagMeaning
selectedCell is part of the current selection
todayCell is today
disabledCell is disabled by disabled / min / max rules
excludedCell is cut out of emitted spans by exclude rules
weekendCell falls on a weekend
inRangeCell is inside the active span
rangeStart / rangeEndCell is a span bound
previewCell is inside the hover preview while drawing a range
outsideCell belongs to an adjacent leading / trailing month

A few things to keep in mind:

  • The cell is the positioning context. To paint a full-cell background (heatmaps, tints), return an absolutely-positioned element with inset: 0 and borderRadius: "inherit" so it follows the appearance radius, then render the number above it with position: "relative".
  • Handle outside explicitly. Usually render just the number so leading / trailing days stay muted and keep the built-in outside-month contrast treatment.
  • Pass a stable reference. Each cell subscribes to its own state bitmask, so only cells whose flags change re-render — but an inline renderDay closure re-renders all 42 cells every pass. Define it at module scope or in useCallback.
  • Keep it pure and cheap. Derive per-day data deterministically - or memoize a lookup - so cells stay stable across renders.

Weather example

Each in-month day shows its number plus a deterministic weather emoji:

SunMonTueWedThuFriSat
tsx
const WEATHER_ICONS = ["☀️", "⛅", "☁️", "🌧", "⛈", "❄️"]; // Stable per-day value so a given date always renders the same icon. // renderDay hands you a CalendarDate ({ year, month, day }, month 1-12). const seededRandom = (d: CalendarDate) => { const seed = d.year * 10000 + d.month * 100 + d.day; const x = Math.sin(seed) * 10000; return x - Math.floor(x); }; const weatherFor = (d: CalendarDate) => WEATHER_ICONS[Math.floor(seededRandom(d) * WEATHER_ICONS.length)]; const config = createCalendarConfig(); <Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays renderDay={(d, state) => { if (state.outside) return <span>{d.day}</span>; return ( <span style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 2, lineHeight: 1.1, }} > <span style={{ fontSize: 13 }}>{d.day}</span> <span aria-hidden style={{ fontSize: 13 }}> {weatherFor(d)} </span> </span> ); }} /> </Calendar>

The same pattern drives the heatmap, ticket-price, and event-dot recipes on the examples page.

Disabled dates

Pass a DateRuleConfig object (or a precompiled createDisabled() engine) to disabled in createCalendarConfig(). Rules combine with OR logic: a date is disabled if any rule matches it. The same rule shape powers exclude — days that stay spannable but are cut out of the emitted spans (business-day flows), with the resulting segments reported in the onChange details.

Disabled dates example

Open
SunMonTueWedThuFriSat
tsx
import { Calendar, createCalendarConfig } from "@dateforge/react-calendar"; const config = createCalendarConfig({ disabled: { weekends: true, before: new Date(), dates: [new Date(2026, 5, 10), new Date(2026, 5, 11)], }, }); <Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> </Calendar>
OptionTypeDescription
weekendsbooleanDisable Saturday and Sunday
weekdaysnumber[]Disable specific weekdays (0 = Sun, 6 = Sat)
beforeDateDisable all dates before this date
afterDateDisable all dates after this date
datesDate[]Disable individual dates
rangesArray<{ start: Date; end: Date }>Disable date ranges
predicate(date: CalendarDate) => booleanArbitrary match — evaluated last, never cached
allbooleanDisable every date (use with readOnly for view-only calendars)
tsx
import { createCalendarConfig, createDisabled } from "@dateforge/react-calendar"; const config = createCalendarConfig({ disabled: createDisabled({ weekdays: [0, 6], before: startOfToday, ranges: [{ start: new Date(2026, 5, 20), end: new Date(2026, 5, 25) }], }), exclude: { weekends: true }, // cut from emitted spans excludedEndpointPolicy: "snap-inward", // or "reject" });

Malformed rules never throw — they degrade with a fix-oriented dev warning.

Custom presets

CalendarPresets accepts a presets array of PresetInput objects (compiled via definePreset() — passing the plain object works too). The package does not mount defaults for you; import commonPresets / relativePresets or define your own. Two forms exist: a simple offset-based definition and an advanced function-based definition for dynamic or computed ranges. Every preset validates through the core: entries blocked by disabled/min/max or incompatible with the active mode render disabled.

Simple preset - value is a day offset from today (negative = past) or a fixed Date. Optional range extends it into a range of that many days.

Advanced preset - getValue receives { now } and returns a Date, a { from, to } span, or null to hide the preset dynamically.

Holiday presets example

Open
SunMonTueWedThuFriSat
tsx
import { type PresetInput } from "@dateforge/react-calendar"; const holidayPresets: PresetInput[] = [ // Simple — jump to a fixed date { label: "New Year's Day", value: new Date(2027, 0, 1) }, { label: "Christmas", value: new Date(2026, 11, 25) }, // Advanced — computed range { id: "holiday-season", label: "Holiday season", getValue: () => ({ from: new Date(2026, 11, 24), to: new Date(2027, 0, 2) }), }, // Advanced — dynamic: always resolves to next weekend { id: "next-weekend", label: "Next weekend", getValue: ({ now }) => { const daysToSat = ((6 - now.getDay() + 7) % 7) || 7; const sat = new Date(now); sat.setDate(now.getDate() + daysToSat); const sun = new Date(sat); sun.setDate(sat.getDate() + 1); return { from: sat, to: sun }; }, }, ]; <Calendar config={config} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarPresets presets={holidayPresets} /> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> </Calendar>
FieldSimpleAdvancedDescription
labelrequiredrequiredDisplay text in the preset button
idoptionalrequiredStable key for active-state tracking
valuerequired-Day offset (number) or fixed Date
rangeoptional-Extend into a range of N days after value
getValue-requiredFunction returning Date, { from, to }, or null

Design system

Styling is split into two independent axes: theme and appearance. A theme controls color. An appearance controls structure: radius, spacing, density, border feel, shadows, and motion duration. Any theme can combine with any appearance, so product teams can keep one interaction model and change the surface to match different screens.

The calendar wrapper exposes styling through data attributes.

AttributeValuesWhat it controls
data-themebuilt-in family name (custom themes apply inline light-dark() vars)Palette tokens
data-schemelight, dark (resolved from scheme)Which side of the family renders
data-appearancebuilt-in appearance name (custom ones apply inline --cal-* vars)Shape, density, motion, shadows
data-readonlypresent when readOnly is trueDisabled interaction styling

Light and dark: the scheme prop

Every theme is a light/dark family — the scheme prop picks the side:

ValueBehaviorUse when
"auto" (default)CSS color-scheme + light-dark() resolve from the OS - no flash on SSRPublic apps and docs
"light"Pin the light variantLight-only surfaces
"dark"Pin the dark variantDark dashboards, command tools
tsx
import { nebula } from "@dateforge/react-calendar/themes"; <Calendar config={config} theme={nebula} /> // auto - follows the OS <Calendar config={config} theme={nebula} scheme="dark" /> // always dark variant <Calendar config={config} theme={nebula} scheme="light" /> // always light variant

Uncontrolled: CalendarToolbarThemeToggle flips the scheme internally, seeded from the scheme prop. Controlled: pass scheme and onSchemeChange to own the light/dark state — the toggle then reports the next scheme instead of flipping itself. Changing the scheme prop on an uncontrolled calendar is ignored (the seed is read once) and logs a dev warning — add onSchemeChange if you want to drive it from outside.

Built-in theme toggle button

Add <CalendarToolbarThemeToggle /> to your toolbar to give users a light/dark switch inside the calendar - no external state required. The toggle manages its own dark/light mode internally and switches between the two variants of whatever ThemeFamily is active.

Open
SunMonTueWedThuFriSat
tsx
import { nebula } from "@dateforge/react-calendar/themes"; <Calendar config={config} theme={nebula} value={date} onChange={(value) => setDate(value as Date | null)}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> <CalendarToolbarThemeToggle /> </CalendarToolbar> <CalendarDays /> </Calendar>

The button label updates automatically: themeSwitchToDarkLabel when light, themeSwitchToLightLabel when dark. Override both on <Calendar> for localization, or directly on the toggle component for per-instance wording.

Built-in themes

28 theme families, each with a light and dark variant. Use the name string (theme="dracula", generated stylesheet, all 28 reachable) or import the family object from the /themes barrel (tree-shaken — recommended for production). Pick the variant with scheme, or leave "auto".

noir, espresso, meadow, fjord, velvet, crimson, solar, nebula, neon, prism, slate, pearl, sandstone, bauhaus, monsoon, industrial, snow, eclipse, chalk, temporal, riso, cyber, split, aurora, graphite, dracula, mint, abyss

Browse all themes in the interactive playground →

Import named families from the barrel when you know what you need:

Monsoon theme

Open
SunMonTueWedThuFriSat
tsx
import { monsoon } from "@dateforge/react-calendar/themes"; <Calendar config={config} theme={monsoon}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> </Calendar> {/* Or by name — every built-in family works as a string */} <Calendar config={config} theme="monsoon" />

The barrel is tree-shakeable — importing { monsoon } bundles only that family. String names (theme="monsoon") trade bundle size for runtime flexibility: the generated stylesheet keeps all 28 reachable.

Creating themes

Use createTheme() when your product has brand tokens that do not match a built-in palette. Pass shared tokens at the root level and override per-variant with light / dark keys:

tsx
import { Calendar, createTheme } from "@dateforge/react-calendar"; const brandTheme = createTheme({ accent: "#2563eb", range: "#22c55e", weekend: "#ef4444", light: { backdrop: "#f8fafc", text: "#18181b" }, dark: { backdrop: "#0f172a", text: "#f8fafc" }, }); <Calendar config={config} theme={brandTheme} /> // auto <Calendar config={config} theme={brandTheme} scheme="dark" /> // always dark

createTheme always returns a light/dark family: top-level tokens are shared, light / dark override per side. Omitted companions (like activeText for your accent) are derived with WCAG-compliant contrast.

Custom theme

Open
SunMonTueWedThuFriSat
tsx
import { Calendar, createTheme } from "@dateforge/react-calendar"; // Shared tokens apply to both variants. // light / dark keys override per variant. const brandTheme = createTheme({ accent: "#1ad980", range: "#a7f3d0", weekend: "#dc2626", light: { backdrop: "#ffffff", text: "#18181b", tone: "#f0fdf4", stroke: "#d4d4d8", }, dark: { backdrop: "#0a1a12", text: "#f0fdf4", tone: "#14532d", stroke: "#166534", }, }); <Calendar config={config} theme={brandTheme}> {/* scheme="auto" — follows the OS */} <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> </Calendar> <Calendar config={config} theme={brandTheme} scheme="dark" /> {/* always dark variant */} <Calendar config={config} theme={brandTheme} scheme="light" /> {/* always light variant */}

Per-module theme override

Every module accepts theme (a built-in family name) and scheme props that override the Calendar-level styling for that module only. This lets you mix palettes inside one picker - for example a dark toolbar on a light calendar, or a branded info strip that matches your sidebar.

tsx
import { snow } from "@dateforge/react-calendar/themes"; // Calendar = snow (light). Toolbar = noir dark. Days inherit snow. Info = nebula. <Calendar config={config} theme={snow} scheme="light"> <CalendarToolbar theme="noir" scheme="dark"> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> <CalendarInfo theme="nebula" showSummary showRelative /> </Calendar>
Open
SunMonTueWedThuFriSat
May 13, 2026
tsx
import { snow } from "@dateforge/react-calendar/themes"; // Calendar = snow (light). Toolbar overrides to noir dark. // CalendarInfo overrides to nebula. Days inherit snow from Calendar. // Modules take a STRING theme name (+ optional scheme); objects stay on the root. <Calendar config={config} theme={snow} scheme="light" value={date} onChange={handleChange}> <CalendarToolbar theme="noir" scheme="dark"> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> <CalendarInfo theme="nebula" showSummary showRelative /> </Calendar>

Priority chain: module theme/scheme → Calendar theme/scheme → built-in default.

Design tokens

Styles layer in this order - each outer layer refines without accidentally overriding inner ones:

text
@layer cal-base, cal-themes, cal-appearances, cal-modules, cal-user;
  • cal-base - reset, token declarations, defaults, shell layout
  • cal-themes - color tokens (--c-*) per family, light + dark variants
  • cal-appearances - shape/spacing/motion tokens (--cal-*) per appearance
  • cal-modules - module layout, selection/hover state
  • cal-user - supported consumer escape hatch

Unlayered app CSS still wins over all library layers. Prefer createTheme(), createAppearance(), and stable data-* attributes before reaching for cal-user. The library uses zero !important.

Color tokens

v3 token keys map to long, readable CSS vars (--c-accent, not v2's --c-a). Three keys were renamed from v2 because the old names lied about their role: v2 highlight → v3 accent, v2 accent → v3 focusRing; tone stayed.

Token (--c-*)Role
accentBrand color - selected cells, active drum items (v2 highlight)
activeTextInk on top of accent (selected-cell text, 4.5:1 contrast)
todayDotToday marker color
backdropRoot shell background
toneSubtle surface - hover fills, secondary chips
textMain ink
strokeBorders and separators
shadowShadow color (usually the accent at low alpha)
disabledDisabled surface
mutedTextSecondary ink (out-of-focus labels)
disabledTextDisabled ink (readable, 3:1+)
weekendWeekend ink - surfaces derive tints from it
rangeRange fill color
errorValidation / error ink
outOfMonthOptional ink for days outside the viewed month (falls back to mutedText)
focusRingOptional focus outline ink, derived from activeText when omitted (v2 accent)

Built-in families pass a WCAG contrast audit in both schemes: primary text at 4.5:1, low-emphasis inks at 3:1 minimum.

Appearances

Appearances are structural presets. They are useful when the same date workflow appears in different surfaces: a dense table filter, a friendly booking flow, a touch-first scheduler, or a sharp internal tool.

Try all appearances in the interactive playground →

Appearance tokens

Shape, spacing, density, and motion tokens set by createAppearance() and built-in appearance presets.

Token (--cal-*)Role
radius / containerRadiusBase border-radius / outer shell radius
border / controlBorderContainer stroke width / button stroke width
spacing / containerGapBase gap & padding unit / gap between module containers
daysGap / daysPaddingGap between day cells / day-grid padding
dayHeightDay cell height floor (min-block-size), e.g. "3em" roomy, "2em" tight (replaces v2 dayRatio)
popupPadding / tilePadding / controlPaddingPopup, tile, and button padding
chipSizeSelected-date chip size
font / fontSize / dayFontSize / dayWeight / controlWeight / letterSpacingTypography axis
shadowSm / shadowMd / shadowLgDepth scale (uses the theme shadow token)
transition / easingMotion duration and curve
pressScale:active squish scale for buttons/tiles ("0.95" tactile, "1" none)
opacityDisabled / opacityMuted / opacityHoverState opacity scale

All partial — createAppearance() ignores unknown keys, it never throws.

Typography tokens

Set by the core layout module (layout.module.css), not by appearance presets. Read-only for consumers - use createAppearance() for size/density, not direct overrides.

TokenRole
--cal-font-sizeContainer-relative base: clamp(11px, 2.7cqw, 18px)
--cal-text-dayAdaptive day-cell text: clamp(0.72em, …, 1.15em)
--cal-text-2xs--cal-text-lgSemantic scale (0.6em – 0.95em)
--cal-weight-regular--cal-weight-boldFont weights 400 – 700
--cal-leading-tight--cal-leading-relaxedLine heights 1 – 1.6

Built-in appearances

AppearanceCharacterGood for
zenithPolished, balanced, flagship default feelGeneral product pickers (new in v3)
compactDense, tight, minimal paddingDashboards, sidebars, data-heavy tools
squareSharp corners, minimal shadowsEnterprise UI, grids, internal tools
softBalanced spacing and gentle roundingDefault product pickers
bubbleSpacious, rounded, prominent shadowsConsumer flows and friendly surfaces
loftAiry, relaxed, large touch targetsEditorial, scheduling, touch-first UI
airyOpen, minimal, low-shadowLarge surfaces and calm scheduling flows
pressEditorial, serif, print-like rhythmArticle pages, launches, branded storytelling

Use them by name (appearance="zenith") or import the object from @dateforge/react-calendar/appearances — the barrel is tree-shakeable. Omit the prop for the v3 default look.

Bubble appearance

Open
SunMonTueWedThuFriSat
tsx
import { bubble } from "@dateforge/react-calendar/appearances"; <Calendar config={config} appearance={bubble}> <CalendarToolbar> <CalendarToolbarPrev /> <CalendarToolbarMonthTrigger /> <CalendarToolbarNext /> <CalendarToolbarYearTrigger compact /> </CalendarToolbar> <CalendarDays /> </Calendar> {/* Or by name */} <Calendar config={config} appearance="bubble" />

Custom appearances are best when density, rhythm, or shape is part of the brand system.

tsx
import { createAppearance } from "@dateforge/react-calendar"; const dense = createAppearance({ radius: "0.35em", spacing: "0.45em", dayHeight: "2.2em", transition: "0.14s", });