DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Calendar

Date grid built on react-day-picker v9. Single, range and multiple selection, dropdown month/year captions, day markers and per-day content, unavailable days, RTL and localized chrome, week numbers, multi-month, an optional date-time panel, and a skeleton twin.

Playground

Installation

pnpm add @tessinaui/ui react-day-picker

Usage

import { Calendar } from "@tessinaui/ui";
{/* Single date */}
<Calendar mode="single" selected={date} onSelect={setDate} />

{/* Date range */}
<Calendar mode="range" selected={range} onSelect={setRange} numberOfMonths={2} />

{/* Multiple dates */}
<Calendar mode="multiple" selected={dates} onSelect={setDates} />

A calendar with no mode (and no onDayClick) renders a display-only grid: day cells are plain text, not buttons. markers, renderDayExtra and unavailableDates need the interactive grid, so pass a mode with them — the component warns in development if you don't.

Examples

Default

A single-date picker — the simplest real usage.

Range

Select a start and end date across two months with mode="range".

Range limits

min/max bound the span in days and excludeDisabled truncates a range that would swallow a disabled day.

Multiple

Pick several independent dates with mode="multiple".

Required

required makes the selection non-clearable — re-clicking the selected day keeps it, so onSelect never reports undefined once a value exists.

Month and year dropdowns

captionLayout="dropdown" swaps the caption text for month + year selects — the far-jump layout for dates of birth and archives. dropdown-months / dropdown-years render just one select; reverseYears lists years newest first. Option ranges follow fromDate/toDate (default: the last 100 years).

Markers

markers paints a dot under matching days. A label on an entry names its meaning twice from one source: in the day's accessible name ("…, Deadline") and in the showLegend footer row of intent-coloured swatches — so the legend can never drift from the grid. Unlabelled entries fall back to the generic dayMarked suffix and stay out of the legend.

Day content

renderDayExtra renders a second line inside every day cell — prices, availability, counts. Days where the callback returns null keep a muted "–" so the day numbers stay aligned across the row. Combined with unavailableDates the line can label sold-out days ("Sold") — the strike hits the day number only. The line is aria-hidden; keep essential figures available elsewhere.

Trade / P&L heat map

Custom modifiers + modifiersClassNames tint the day fills, renderDayExtra prints the value — the merge pipeline means this is pure composition, no component changes.

Goal rings

A components.DayButton override draws an SVG progress ring behind each number (fitness-app style) while markers, selection and keyboard behaviour keep working. Pairs well with selectionVariant="outline".

Size the ring explicitly — absolute inset-0 size-full p-1, the gap as padding. iOS Safari sizes an absolutely positioned <svg> with an auto width to its containing block and ignores the end-side inset, so absolute inset-1 alone drew a cell-sized ring 4px low-right of the number on every iPhone while Chrome, Firefox and WebKit trunk stretched it to the inset box.

The ring is centred on the button, so the number has to be too: keep the house number-span classes. tabular-nums centres each digit in one shared advance (Inter's proportional 1 is narrow and sits left of its own), and the absolute + translate centring exists for phones: iOS WebKit snaps a layout-positioned text run to whole device pixels, so a fractional half-advance floored the number up to a device pixel high-left of the ring. A composited transform (will-change-transform forces the layer) rasters the glyphs at an integer origin and places the layer at the true fractional centre.

Booked (unavailable) days

unavailableDates is the reachable but taken state: struck through, still focusable (screen readers can land on them and hear aria-disabled), and never selectable. Compare disabled — the greyed can't reach state.

Today button and footer

showTodayButton renders a footer action that returns to the current month and hides itself when today sits outside fromDate/toDate. The footer slot composes next to it.

Week numbers

showWeekNumber adds the gutter column; ISOWeek switches numbering and the week start to ISO-8601.

Localization

locale drives the caption, the dropdowns and the weekday cells through Intl; dir="rtl" alone defaults to Arabic. strings overrides any chrome label. For full control of react-day-picker's own day labels, pass its labels/locale objects — they merge with ours.

Sizes

Three day-cell densities — sm, md, and lg.

Disabled dates

Block specific days from selection — here weekends are disabled.

Loading

Use CalendarSkeleton as a placeholder while date data loads.

API Reference

Calendar props

All react-day-picker DayPicker props pass through in addition to the props below — components, formatters and classNames merge per key with the house implementation instead of replacing it wholesale.

Style

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"Day-cell density: 32/36/40px on desktop; every tappable cell floors at 44px below md (WCAG 2.5.5).
width"narrow" | "default" | "wide""default"Container padding. Phones always use the narrow p-1 so the 44px grid fits 320px; desktop gets p-1 / p-3 / p-5.
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""md"Frame radius 0/8/12/16/24/24px — full is the §2 panel cap (24px); day buttons go fully circular there.
fullWidthbooleanfalseGrid stretches to the container. Replaces stretch (deprecated alias, removal next minor).
classNamestring—Added to the outer frame. Other HTML attributes (data-*, aria-*, id, style) land on react-day-picker's grid root, mirroring the form-control forwarding exception.

Selection

PropTypeDescription
mode"single" | "range" | "multiple"Selection behaviour. Omit for a display-only grid.
selectedDate | DateRange | Date[]Controlled selected value
onSelect(value, triggerDate, modifiers, e) => voidSelection callback. Never fires for unavailableDates.
requiredbooleanNon-clearable selection (per mode)
min / maxnumbermultiple: selection count bounds · range: span bounds in days (endpoints counted)
excludeDisabledbooleanrange: truncate instead of spanning a disabled day
defaultMonthDateMonth shown on first render
todayDateOverride "today" (deterministic tests, server clocks)
selectionVariant"fill" | "outline" | "line"How selection paints: solid chip + band (default) · 2px ring, no fill · underline bar (ranges draw one continuous line). Clicking an adjacent-month day navigates to that month.

Constraints

PropTypeDescription
fromDate / toDateDateInclusive earliest/latest selectable day. Clamps navigation to the bound months and disables out-of-range days inside them.
startMonth / endMonthDateNavigation bounds only (no day disabling) — take precedence over the pair above for navigation.
disabledMatcher | Matcher[]Not selectable, greyed, skipped by pointer. Merged with the fromDate/toDate clamp.
unavailableDatesMatcher | Matcher[]Focusable but not selectable — struck through, aria-disabled (booked / sold out).
hiddenMatcher | Matcher[]Days removed from the grid entirely.

Matcher is react-day-picker's vocabulary — a Date, Date[], { from, to }, { before }, { after }, { dayOfWeek: number[] }, or a (date: Date) => boolean predicate. Constraints govern this grid only — a paired text input (DatePicker) still needs its own validation.

Caption & navigation

PropTypeDefaultDescription
captionLayout"label" | "dropdown" | "dropdown-months" | "dropdown-years""label"Caption chrome. Dropdown modes keep the chevrons for single steps.
reverseYearsbooleanfalseYear dropdown descending (date-of-birth)
month / onMonthChangeDate / (m: Date) => void—Controlled displayed month
showTodayButtonbooleanfalseFooter "Today" action; auto-hidden when today is out of bounds
disableNavigationbooleanfalseLock the displayed month
pagedNavigationbooleanfalseMulti-month: advance by the whole page

Layout

PropTypeDefaultDescription
numberOfMonthsnumber1Months side by side — stacks vertically on phones
showOutsideDaysbooleantrueShow adjacent-month days
fixedWeeksbooleanfalseAlways 6 rows — no height jitter between months
showWeekNumberbooleanfalseWeek-number gutter column
ISOWeekbooleanfalseISO-8601 weeks (Monday start, ISO numbering)
weekStartsOn0–6localeFirst day of week
hideWeekdaysbooleanfalseRemove the weekday header row

Day content

PropTypeDescription
markersMatcher | { match: Matcher; intent?: "none" | "error" | "warning" | "success" | "info"; label?: string }[]Dot under matching days; label becomes the accessible-name suffix and the legend entry
showLegendbooleanFooter swatch row from the labelled markers entries (composes with footer/showTodayButton)
renderDayExtra(date, modifiers) => ReactNodeSecond line on every day cell (aria-hidden); nullish returns render a muted "–" so numbers align; cells grow to fit
footerReactNodeRow under the grid — plain content, not a live region

Localization

PropTypeDefaultDescription
dir"ltr" | "rtl""ltr"Flips arrows and layout; RTL defaults locale to Arabic
localestring"en-US"BCP-47 tag for caption, dropdowns and weekday cells (western digits kept)
stringsPartial<CalendarStrings>—Override any chrome label — nav, dropdowns, panel, dayMarked, today
timeZonestring—react-day-picker time-zone handling — detect on the client (in an effect) to avoid hydration mismatch
numeralsNumerals"latn"Digit system for day numbers

Date & Time Panel

PropTypeDefaultDescription
showDateTimebooleanfalseBuilt-in Start/End wells with Apply — the date well opens a nested house calendar, the time well is the library TimePicker (popover wheel); both follow rounded
dateTimeLayout"vertical" | "horizontal""vertical"Panel below or beside the grid (side-by-side stacks on phones)
showTimebooleantrueTime inputs next to the dates
showTimezonebooleanfalseSelectable IANA timezone under Apply (platform zone list; detected zone by default)
timezone / onTimezoneChangestring / (zone) => voiddetectedControlled zone for the footer select; the choice rides CalendarApplyData.timezone
onApply(data: CalendarApplyData) => void—Apply click. In multiple mode data.dates carries every pick, sorted.

Typing an end date before the start swaps the endpoints; a start typed past the end restarts the range. A selection that carries a real time of day seeds the time inputs.

CalendarSkeleton props

PropTypeDefaultDescription
size / width / rounded / dir / numberOfMonths / fullWidthas Calendar—The placeholder is the calendar's own box (§2b)
monthDatetodayMonth the placeholder stands in for — drives the week-row count
weeksnumberderivedPin the row count (pass 6 to mirror fixedWeeks)
weekStartsOn0–60Keeps the derived row count in step with the real grid

Accessibility

  • Grid semantics come from react-day-picker: role="grid", one roving tab stop, and full day names on every cell ("Friday, August 14th, 2026").
  • Keyboard: ←/→ day (RTL-aware) · ↑/↓ week · PageUp/PageDown month · Shift+PageUp/Shift+PageDown year · Home/End week bounds · Enter/Space select. Caption dropdowns are native selects.
  • One live region: the first month's caption announces navigation (role="status"); dropdown captions announce through the selects instead.
  • Unavailable ≠ disabled: unavailable days stay in the focus order and expose aria-disabled; disabled days are removed from interaction.
  • Every interactive cell floors at 44×44px below md; the panel's inputs hold 16px text on phones so iOS doesn't zoom.
  • The today ring uses the hard-gated --outline-border (≥3:1 both themes); marker dots are supplementary — the accessible name carries the information.

Agent notes (data attributes)

data-slot hooks: calendar, calendar-caption, calendar-caption-label, calendar-nav-previous/next, calendar-dropdown, calendar-day, calendar-day-extra, calendar-footer, calendar-today, calendar-datetime, calendar-apply, calendar-skeleton. Day buttons expose data-selected / data-today / data-outside / data-disabled / data-range-start/middle/end / data-unavailable / data-marked when markers, day content or unavailable dates are active.

CalendarApplyData type

import type { CalendarApplyData } from "@tessinaui/ui";

interface CalendarApplyData {
  start?: Date;
  startTime?: string;  // "HH:MM"
  end?: Date;
  endTime?: string;    // "HH:MM"
  dates?: Date[];      // multiple mode: every pick, sorted
  timezone?: string;   // showTimezone: the selected IANA zone
}

DateRange type

import type { DateRange } from "@tessinaui/ui";

interface DateRange {
  from: Date | undefined;
  to?: Date | undefined;
}
ButtonGroupCard

On this page

PlaygroundInstallationUsageExamplesDefaultRangeRange limitsMultipleRequiredMonth and year dropdownsMarkersDay contentTrade / P&L heat mapGoal ringsBooked (unavailable) daysToday button and footerWeek numbersLocalizationSizesDisabled datesLoadingAPI ReferenceCalendar propsStyleSelectionConstraintsCaption & navigationLayoutDay contentLocalizationDate & Time PanelCalendarSkeleton propsAccessibilityAgent notes (data attributes)CalendarApplyData typeDateRange type