Add this component

npx zentauri-ui add calendar

Accessibility notes

Keyboard accessible by default with semantic markup, visible focus treatment, and tokenized states. Add descriptive labels for icon-only or decorative usage.

Dependency notes

Check Installation for shared peers. This component keeps styling in Tailwind classes and the --zui-* token contract.

Forms

Calendar with locale-aware month grids

Use the calendar component for inline date selection: booking flows, availability views, and scheduling UIs. Month names, weekday headers, first-day-of-week, and even digits all come from the platform's Intl APIs, so a single locale prop localizes the whole grid with no date library.

July 2026
SunMonTueWedThuFriSat
July 2026
SunMonTueWedThuFriSat

Calendar variants playground

Pick a selection mode, appearance, size, caption layout, and animation to preview the calendar live. Toggle Show output / Show code and the snippet updates to match the selected variant.


July 2026
SunMonTueWedThuFriSat

One locale prop, fully localized

Month names, weekday headers, first-day-of-week, and even the digits come from the platform's Intl APIs — no date library, no locale bundles.

English (US) · en-US

July 2026
SunMonTueWedThuFriSat

Deutsch · de-DE

Juli 2026
MoDiMiDoFrSaSo

العربية (مصر) · ar-EG

يوليو ٢٠٢٦
السبتالأحدالاثنينالثلاثاءالأربعاءالخميسالجمعة

日本語 · ja-JP

2026年7月

Calendar API

Generated from the package prop types and variant definitions.

Calendar

CalendarProps

Variants

PropTypeDefault
size
lgmdsm
md

State

PropTypeDefault
defaultValueDate | Date[] | DateRange | undefinednone
onValueChange((value: Date | undefined) => void) | ((value: Date[]) => void) | ((value: DateRange | undefined) => void) | undefinednone
valueDate | Date[] | DateRange | undefinednone

Behavior

PropTypeDefault
appearance'default' | 'outline' | 'ghost' | 'blue' | 'cyan' | 'green' | 'lime' | 'mint' | 'ocean' | 'sapphire' | 'lavender' | 'ruby' | 'red' | 'slate' | 'zinc' | 'stone' | 'royal' | 'electric' | 'forest' | 'sunset' | 'magenta' | 'crimson' | 'aqua' | 'plum' | 'sky' | 'rose' | 'purple' | 'pink' | 'orange' | 'yellow' | 'teal' | 'indigo' | 'emerald' | 'gradient-blue' | 'gradient-green' | 'gradient-red' | 'gradient-yellow' | 'gradient-purple' | 'gradient-teal' | 'gradient-indigo' | 'gradient-pink' | 'gradient-orange' | 'glass' | 'secondary' | 'destructive' | null | undefinednone
captionLayoutMonth/year title vs select dropdowns. Default `"label"`.'label' | 'dropdowns' | undefinednone
defaultMonthDefault visible month: the selected value's month, else today.Date | undefinednone
disabledDisabled days (distinct from min/max): focusable but not selectable.DateMatcher | DateMatcher[] | undefinednone
firstDayOfWeek0 = Sunday … 6 = Saturday. Default: locale's `Intl` week info.0 | 1 | 4 | 6 | 2 | 3 | 5 | undefinednone
fixedWeeksAlways 6 rows for stable height across months. Default false.boolean | undefinednone
fromYearDropdown year range lower bound. Default: today's year - 100.number | undefinednone
hiddenDays rendered blank.DateMatcher | DateMatcher[] | undefinednone
localeBCP 47 locale tag. Default: runtime locale.string | undefinednone
maxMaximum number of selected days; further clicks no-op.number | undefinednone
maxDateNavigation + selection upper bound.Date | undefinednone
maxRangeDaysMaximum range span in days (inclusive of both ends).number | undefinednone
minDateNavigation + selection lower bound.Date | undefinednone
minRangeDaysMinimum range span in days (inclusive of both ends).number | undefinednone
modeSelection mode. Default `"single"`. Discriminates the `value` types.'single' | 'multiple' | 'range' | undefinednone
monthControlled visible month (any day within it).Date | undefinednone
numberOfMonthsMonths rendered side by side. Default 1, capped at 3.number | undefinednone
onMonthChange((month: Date) => void) | undefinednone
renderDayPer-day custom render (dots, prices, tooltips).((day: Date, state: CalendarDayState) => ReactNode) | undefinednone
requiredWhen false (default), clicking a selected day deselects it. In `range` mode this applies to a *completed* range: clicking either endpoint clears the whole range; clicking any other day always starts a new one.boolean | undefinednone
showOutsideDaysShow trailing/leading days of adjacent months. Default true.boolean | undefinednone
showWeekNumbersShow an ISO week number column. Default false.boolean | undefinednone
todayToday's date override — required for deterministic tests/SSR snapshots. Default: `new Date()` computed once on mount.Date | undefinednone
toYearDropdown year range upper bound. Default: today's year + 100.number | undefinednone
weekdayFormatWeekday header label format. Default `"short"`.'long' | 'short' | 'narrow' | undefinednone
Inherited HTML props
PropTypeDefault
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

CalendarCommon

CalendarCommonProps

Behavior

PropTypeDefault
appearance'default' | 'outline' | 'ghost' | 'blue' | 'cyan' | 'green' | 'lime' | 'mint' | 'ocean' | 'sapphire' | 'lavender' | 'ruby' | 'red' | 'slate' | 'zinc' | 'stone' | 'royal' | 'electric' | 'forest' | 'sunset' | 'magenta' | 'crimson' | 'aqua' | 'plum' | 'sky' | 'rose' | 'purple' | 'pink' | 'orange' | 'yellow' | 'teal' | 'indigo' | 'emerald' | 'gradient-blue' | 'gradient-green' | 'gradient-red' | 'gradient-yellow' | 'gradient-purple' | 'gradient-teal' | 'gradient-indigo' | 'gradient-pink' | 'gradient-orange' | 'glass' | 'secondary' | 'destructive' | null | undefinednone
captionLayoutMonth/year title vs select dropdowns. Default `"label"`.'label' | 'dropdowns' | undefinednone
defaultMonthDefault visible month: the selected value's month, else today.Date | undefinednone
disabledDisabled days (distinct from min/max): focusable but not selectable.DateMatcher | DateMatcher[] | undefinednone
firstDayOfWeek0 = Sunday … 6 = Saturday. Default: locale's `Intl` week info.0 | 1 | 4 | 6 | 2 | 3 | 5 | undefinednone
fixedWeeksAlways 6 rows for stable height across months. Default false.boolean | undefinednone
fromYearDropdown year range lower bound. Default: today's year - 100.number | undefinednone
hiddenDays rendered blank.DateMatcher | DateMatcher[] | undefinednone
localeBCP 47 locale tag. Default: runtime locale.string | undefinednone
maxDateNavigation + selection upper bound.Date | undefinednone
minDateNavigation + selection lower bound.Date | undefinednone
monthControlled visible month (any day within it).Date | undefinednone
numberOfMonthsMonths rendered side by side. Default 1, capped at 3.number | undefinednone
onMonthChange((month: Date) => void) | undefinednone
renderDayPer-day custom render (dots, prices, tooltips).((day: Date, state: CalendarDayState) => ReactNode) | undefinednone
requiredWhen false (default), clicking a selected day deselects it. In `range` mode this applies to a *completed* range: clicking either endpoint clears the whole range; clicking any other day always starts a new one.boolean | undefinednone
showOutsideDaysShow trailing/leading days of adjacent months. Default true.boolean | undefinednone
showWeekNumbersShow an ISO week number column. Default false.boolean | undefinednone
size'md' | 'sm' | 'lg' | null | undefinednone
todayToday's date override — required for deterministic tests/SSR snapshots. Default: `new Date()` computed once on mount.Date | undefinednone
toYearDropdown year range upper bound. Default: today's year + 100.number | undefinednone
weekdayFormatWeekday header label format. Default `"short"`.'long' | 'short' | 'narrow' | undefinednone
Inherited HTML props
PropTypeDefault
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

CalendarSelection

CalendarSelectionProps

State

PropTypeDefault
defaultValueDate | Date[] | DateRange | undefinednone
onValueChange((value: Date | undefined) => void) | ((value: Date[]) => void) | ((value: DateRange | undefined) => void) | undefinednone
valueDate | Date[] | DateRange | undefinednone

Behavior

PropTypeDefault
maxMaximum number of selected days; further clicks no-op.number | undefinednone
maxRangeDaysMaximum range span in days (inclusive of both ends).number | undefinednone
minRangeDaysMinimum range span in days (inclusive of both ends).number | undefinednone
modeSelection mode. Default `"single"`. Discriminates the `value` types.'single' | 'multiple' | 'range' | undefinednone

CalendarVariant

CalendarVariantProps

Behavior

PropTypeDefault
appearance'default' | 'outline' | 'ghost' | 'blue' | 'cyan' | 'green' | 'lime' | 'mint' | 'ocean' | 'sapphire' | 'lavender' | 'ruby' | 'red' | 'slate' | 'zinc' | 'stone' | 'royal' | 'electric' | 'forest' | 'sunset' | 'magenta' | 'crimson' | 'aqua' | 'plum' | 'sky' | 'rose' | 'purple' | 'pink' | 'orange' | 'yellow' | 'teal' | 'indigo' | 'emerald' | 'gradient-blue' | 'gradient-green' | 'gradient-red' | 'gradient-yellow' | 'gradient-purple' | 'gradient-teal' | 'gradient-indigo' | 'gradient-pink' | 'gradient-orange' | 'glass' | 'secondary' | 'destructive' | null | undefinednone
size'md' | 'sm' | 'lg' | null | undefinednone

CalendarAnimated

CalendarAnimatedProps

animated

State

PropTypeDefault
defaultValueDate | Date[] | DateRange | undefinednone
onValueChange((value: Date | undefined) => void) | ((value: Date[]) => void) | ((value: DateRange | undefined) => void) | undefinednone
valueDate | Date[] | DateRange | undefinednone

Behavior

PropTypeDefault
animationCalendarAnimation | undefinednone
appearance'default' | 'outline' | 'ghost' | 'blue' | 'cyan' | 'green' | 'lime' | 'mint' | 'ocean' | 'sapphire' | 'lavender' | 'ruby' | 'red' | 'slate' | 'zinc' | 'stone' | 'royal' | 'electric' | 'forest' | 'sunset' | 'magenta' | 'crimson' | 'aqua' | 'plum' | 'sky' | 'rose' | 'purple' | 'pink' | 'orange' | 'yellow' | 'teal' | 'indigo' | 'emerald' | 'gradient-blue' | 'gradient-green' | 'gradient-red' | 'gradient-yellow' | 'gradient-purple' | 'gradient-teal' | 'gradient-indigo' | 'gradient-pink' | 'gradient-orange' | 'glass' | 'secondary' | 'destructive' | null | undefinednone
captionLayoutMonth/year title vs select dropdowns. Default `"label"`.'label' | 'dropdowns' | undefinednone
defaultMonthDefault visible month: the selected value's month, else today.Date | undefinednone
disabledDisabled days (distinct from min/max): focusable but not selectable.DateMatcher | DateMatcher[] | undefinednone
firstDayOfWeek0 = Sunday … 6 = Saturday. Default: locale's `Intl` week info.0 | 1 | 4 | 6 | 2 | 3 | 5 | undefinednone
fixedWeeksAlways 6 rows for stable height across months. Default false.boolean | undefinednone
fromYearDropdown year range lower bound. Default: today's year - 100.number | undefinednone
hiddenDays rendered blank.DateMatcher | DateMatcher[] | undefinednone
localeBCP 47 locale tag. Default: runtime locale.string | undefinednone
maxMaximum number of selected days; further clicks no-op.number | undefinednone
maxDateNavigation + selection upper bound.Date | undefinednone
maxRangeDaysMaximum range span in days (inclusive of both ends).number | undefinednone
minDateNavigation + selection lower bound.Date | undefinednone
minRangeDaysMinimum range span in days (inclusive of both ends).number | undefinednone
modeSelection mode. Default `"single"`. Discriminates the `value` types.'single' | 'multiple' | 'range' | undefinednone
monthControlled visible month (any day within it).Date | undefinednone
numberOfMonthsMonths rendered side by side. Default 1, capped at 3.number | undefinednone
onMonthChange((month: Date) => void) | undefinednone
refAllows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref).Ref<HTMLDivElement> | undefinednone
renderDayPer-day custom render (dots, prices, tooltips).((day: Date, state: CalendarDayState) => ReactNode) | undefinednone
requiredWhen false (default), clicking a selected day deselects it. In `range` mode this applies to a *completed* range: clicking either endpoint clears the whole range; clicking any other day always starts a new one.boolean | undefinednone
showOutsideDaysShow trailing/leading days of adjacent months. Default true.boolean | undefinednone
showWeekNumbersShow an ISO week number column. Default false.boolean | undefinednone
size'md' | 'sm' | 'lg' | null | undefinednone
todayToday's date override — required for deterministic tests/SSR snapshots. Default: `new Date()` computed once on mount.Date | undefinednone
toYearDropdown year range upper bound. Default: today's year + 100.number | undefinednone
weekdayFormatWeekday header label format. Default `"short"`.'long' | 'short' | 'narrow' | undefinednone
Inherited HTML props
PropTypeDefault
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone
CSS variable overrides

Calendar CSS variables

Override these calendar variables on :root, a theme selector, or a component wrapper. Every appearance follows the same *-selected-bg / *-selected-fg / *-range-bg pattern shown for default and blue.

46 variables

Pattern: --zui-<component>-<slot?>-<variant?>-<property>-<state?>-dark?

:root {
  --zui-calendar-bg: #ffffff;
  --zui-calendar-fg: oklch(20.8% 0.042 265.755);
  --zui-calendar-border: #0000001a;
  --zui-calendar-radius: 0.75rem;
  --zui-calendar-p: 0.75rem;
  --zui-calendar-nav-size: 2rem;
  --zui-calendar-nav-border: #0000001a;
  --zui-calendar-caption-font-size: 0.875rem;
  --zui-calendar-dropdown-border: #0000001a;
  --zui-calendar-cell-radius: 0.5rem;
  --zui-calendar-cell-hover-bg: #0000000d;
  --zui-calendar-cell-size-sm: 2rem;
  --zui-calendar-cell-size-md: 2.25rem;
  --zui-calendar-cell-size-lg: 2.75rem;
  --zui-calendar-muted-fg: oklch(44.6% 0.043 257.281);
  --zui-calendar-today-ring: oklch(44.6% 0.043 257.281);
  --zui-calendar-ring-focus: oklch(44.6% 0.043 257.281);
  --zui-calendar-default-selected-bg: oklch(20.8% 0.042 265.755);
  --zui-calendar-default-selected-fg: oklch(98.4% 0.003 247.858);
  --zui-calendar-default-range-bg: color-mix(in oklch, oklch(20.8% 0.042 265.755) 18%, transparent);
  --zui-calendar-blue-selected-bg: #2563eb;
  --zui-calendar-blue-selected-fg: #ffffff;
  --zui-calendar-blue-range-bg: color-mix(in oklch, #2563eb 18%, transparent);
}

/* Dark theme variables follow the same names with -dark appended. */
.dark {
  --zui-calendar-bg-dark: oklch(12.9% 0.042 264.695);
  --zui-calendar-fg-dark: oklch(98.4% 0.003 247.858);
  /* ...same variables with -dark at the end */
}

What it does

The calendar renders one or more month grids with single, multiple, or range selection. Range mode paints hover previews between the endpoints, supports minimum and maximum spans, and swaps endpoints automatically when the second pick is earlier than the first.

Every label — weekday headers, month captions, and day digits — is produced by Intl.DateTimeFormat, so ar-EG renders Arabic-Indic digits and de-DE starts weeks on Monday without configuration.

Composition and API

Selection and the visible month are both controllable. Constrain interaction with minDate/maxDate bounds and disabled matchers (dates, ranges, weekdays, or predicates), render custom day content with renderDay, and switch the caption to month/year dropdowns for birthday-style jumps. The animated entry (imported from the /animated subpath) adds framer-motion month transitions.

Common use cases

  • Inline booking calendars with disabled and price-annotated days.
  • Date range selection for reports, analytics filters, and stays.
  • Multi-date selection for shift planning and event scheduling.
  • Localized calendars that follow the user's language and numbering system.

Accessibility

The grid uses table semantics with role="grid", column headers with long-form abbreviations, aria-selected day cells, and aria-current on today. Disabled days stay focusable but not selectable, matching the ARIA date-grid guidance.

This preview page exposes a single h1 in the hero for a clean outline.

Next.js integration notes

Pass explicit today and locale props in SSR-critical paths so server and client render the same grid. Keep interactive calendars in client components.

Set NEXT_PUBLIC_SITE_URL so canonical and Open Graph URLs resolve on deploy.

FAQ

Does the Calendar component need date-fns or dayjs?

No. All date math is plain Date arithmetic and all localization comes from Intl.DateTimeFormat and Intl.Locale — the component adds zero dependencies.

Is the Calendar keyboard accessible?

Yes. It implements the ARIA grid pattern with a roving tabindex: arrow keys move by day and week, Home/End jump within the week, PageUp/PageDown move months, and Shift+PageUp/PageDown move years.

Can I customize the calendar with Tailwind CSS?

Zentauri UI exposes class-friendly variants, data-state attributes, and --zui-calendar-* CSS variables so you can extend styles with Tailwind utilities without fighting inline styles.