Calendar
Inline month grid on Ark UI's date-picker machine — single or range selection, restriction predicates, event dots, and month/year navigation.
Calendar is the inline month grid — Ark's date-picker machine rendered inline (no
trigger/popover), so it's always visible rather than living behind a field. It renders the same
grid module DatePicker will embed in its popover once that lands. The
value is a native Date | null (or { start, end } under range) — Ark's internal DateValue
representation never leaks out.
import { Calendar } from '@42-central/ui-react/calendar';
<Calendar value={date} onChange={setDate} />Calendar is a bare widget, like Table — there's no built-in label /
description / error. A DatePicker popover (trigger + field wrapper) is
where that belongs.
Playground
Component
Dir
Presets
Modes
Props
Code
<Component numOfMonths={1} />Range & numOfMonths
range selects a { start, end } pair instead of a single date — click once for the start,
again for the end. numOfMonths shows that many months side by side; it defaults to 1 regardless
of range — pass numOfMonths={2} to show a full range without paging.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 1 | 2 | 3 | 4 | 5 |
import { useState } from 'react';
import { Calendar, type DateRangeValue } from '@42-central/ui-react/calendar';
const [range, setRange] = useState<DateRangeValue>({ start: null, end: null });
<Calendar range numOfMonths={2} value={range} onChange={setRange} />Restricting selection
isDateUnavailable takes a plain predicate over a native Date — greyed-out, aria-disabled
days the user can't pick. min / max bound the selectable range the same way.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
<Calendar isDateUnavailable={(date) => date.getDay() === 0 || date.getDay() === 6} />@42-central/ui-react/date-picker re-exports a set of composable predicate helpers — disablePast(),
disableFuture(), disableWeekends(), beforeDate(d), afterDate(d), disableDates([...]),
and anyOf(...) / allOf(...) to combine them — built on the exact same conversion boundary
Calendar uses internally.
The predicate's second argument is the time zone this Calendar resolved,
so a day is read in the same zone the grid labels it in — timeZone="UTC" disables UTC
Saturdays wherever the code runs, not whichever days were weekends on the machine that rendered
it. The helpers above take it from there automatically; reach for it in your own predicate too
rather than date.getDay() whenever you set timeZone:
import { anyOf, disablePast, disableWeekends } from '@42-central/ui-react/date-picker';
<Calendar timeZone="UTC" isDateUnavailable={anyOf(disablePast(), disableWeekends())} />It's optional — omitted (a predicate called on its own, outside a Calendar/DatePicker), it
falls back to the host zone.
Event dots
renderDay(date, { isUnavailable, isSelected }) decorates a specific day's cell — draw a dot,
badge, or any marker from your own event data. It receives a native Date, converted from Ark's
internal value at the same timeZone boundary the rest of the component uses.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
<Calendar
renderDay={(date) =>
hasEvent(date) ? <span className="mt-0.5 block size-1 rounded-full bg-(--c-solid)" /> : null
}
/>Month/year navigation
Click the header label to cycle the view — day → month → year — then pick a year, a month, and
land back on a day grid. MonthSelect / YearSelect dropdowns in the day view offer a
quicker direct jump (shown only when a single month is visible). minView / maxView bound
the levels reachable this way, e.g. minView="month" for a picker that never drills into
individual days.
<Calendar minView="month" maxView="year" />Week numbers
| S | M | T | W | T | F | S | |
|---|---|---|---|---|---|---|---|
| 39 | 27 | 28 | 29 | 30 | 1 | 2 | 3 |
| 40 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 41 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 42 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 43 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
<Calendar withWeekNumbers />Fixed height
Months naturally span 4–6 week rows depending on how the days line up, so the grid's height can
shift while paging between months. fixedWeeks always renders 6 rows (padding with the
adjacent month's days) to keep the height constant.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
1 | 2 | 3 | 4 | 5 | 6 | 7 |
<Calendar fixedWeeks />Sizes
Five sizes — xs through xl — matching the rest of the family.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
Code
<Calendar size="xs" defaultValue={new Date()} />
<Calendar size="sm" defaultValue={new Date()} />
<Calendar size="md" defaultValue={new Date()} />
<Calendar size="lg" defaultValue={new Date()} />
<Calendar size="xl" defaultValue={new Date()} />Controlled
Drive the value from state. onChange hands you the next Date | null (or DateRangeValue
under range).
'use client';
import { useState } from 'react';
import { Calendar } from '@42-central/ui-react/calendar';
export function EvaluationDate() {
const [date, setDate] = useState<Date | null>(null);
return <Calendar value={date} onChange={setDate} />;
}Locale & time zone
locale overrides the active locale context (month/day names, first-day-of-week); timeZone
overrides the time zone used for every Date ↔ DateValue conversion at the boundary. Both fall
back to an IntlProvider above the calendar, so one provider at the app root sets
them for every date component at once. With neither, the time zone follows the host — not Ark's
own machine default of "UTC".
<IntlProvider timeZone="Pacific/Auckland">
<Calendar /> {/* days resolve in Auckland */}
<Calendar timeZone="UTC" /> {/* the prop wins over the provider */}
</IntlProvider>When the zone changes while the calendar is mounted, the selection keeps its instant: the same
Date, shown in the new zone, so near midnight it can move to the next or previous day. That is
not an edit, so onChange doesn't fire, and focus stays where it was. DatePicker behaves the
same way.
Translations
locale drives month/day names and first-day-of-week automatically, but Ark's own built-in
accessible names (day cells, prev/next, the view-cycling header, week numbers) are English by
default — translations overrides them. It's partial: unset keys keep Ark's own English text.
<Calendar
locale="fr-FR"
translations={{
clearTrigger: 'Effacer la sélection',
trigger: (open) => (open ? 'Fermer le calendrier' : 'Ouvrir le calendrier'),
}}
/>API
Prop
Type
testId deliberately isn't wired down to individual day cells. Ark's own date-picker machine
already stamps every day cell with data-part="table-cell-trigger" plus its own date-derived
attributes (data-value, data-today, data-selected, …) for free, so a per-day data-testid
would just duplicate a selector you already have:
// No testId needed — Ark's own attributes already identify a specific day.
page.locator('[data-part="table-cell-trigger"][data-value="2026-08-28"]');See the Testing guide for the full testId convention.
Accessibility
role="grid"with full keyboard navigation from Ark — arrows move between days,PageUp/PageDownstep a month,Home/Endjump to the start/end of the week.- Unavailable and out-of-bounds days carry
aria-disabled; the selected day (or range) carriesaria-selected; today carriesaria-current="date". - The header's view-cycling label and prev/next buttons are real
<button>s with accessible names from Ark's built-in translations — override them withtranslations(see above). - Range selection announces the in-progress start/end via Ark's live region.
Changelog
- #317
Calendar, DatePicker, DataTable's date filter and Schedule's event details follow
IntlProvider'stimeZone(prop, then provider, then host) instead of always reading the host zone - #327
When the time zone changes, the picked date keeps its instant and is shown in the new zone. Nothing remounts,
onChangedoesn't fire, and focus stays where it was
- #274
Mirror under RTL: Schedule's icons, spacing, page turns, chip day moves and edge auto-scroll, and Calendar's prev/next chevrons
- #186
Date restriction predicates resolve in the component's own time zone
TagsInput
Free-form, creatable tags on Ark UI's TagsInput machine — type to mint a tag, no fixed option list. Editable, paste-aware, with Pill-styled chips.
Time Input
HOUR/MINUTE (+ SECOND) + AM/PM time entry composed from NumberInput + SegmentGroup — Ark ships no time-picker machine, so this is the kit's own composed time field.