|Kit

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

Loading preview

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.

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
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.

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.

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

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.

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.

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
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
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
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
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 / PageDown step a month, Home / End jump to the start/end of the week.
  • Unavailable and out-of-bounds days carry aria-disabled; the selected day (or range) carries aria-selected; today carries aria-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 with translations (see above).
  • Range selection announces the in-progress start/end via Ark's live region.

Changelog

v0.19.0
  • Calendar, DatePicker, DataTable's date filter and Schedule's event details follow IntlProvider's timeZone (prop, then provider, then host) instead of always reading the host zone

    #317
  • When the time zone changes, the picked date keeps its instant and is shown in the new zone. Nothing remounts, onChange doesn't fire, and focus stays where it was

    #327
v0.17.1
  • Mirror under RTL: Schedule's icons, spacing, page turns, chip day moves and edge auto-scroll, and Calendar's prev/next chevrons

    #274
v0.12.0
  • Date restriction predicates resolve in the component's own time zone

    #186
v0.3.0
  • showWeekNumbers → withWeekNumbers

    #25
  • translations overrides the built-in English microcopy, including in DataTable's date filters

    #24
  • The multi-month nav no longer overlaps prev/next or widens the popover

    #23
v0.2.0
  • New Calendar: an inline month grid for single dates or ranges, across one or more months

    #17
  • fixedWeeks keeps the grid at the same height every month

    #18

On this page