Calendar
A month, and the day chosen in it.
Overview
A month, and the day chosen in it.
First example
import { Calendar } from '@xaui/native/calendar'export function Example() {return <Calendar />}
Anatomy
<Calendar value={date} onValueChange={setDate}>
<Calendar.Header>
<Calendar.PreviousButton accessibilityLabel="Mois précédent" />
<Calendar.Title />
<Calendar.NextButton accessibilityLabel="Mois suivant" />
</Calendar.Header>
<Calendar.Weekdays />
<Calendar.Grid />
</Calendar>
| slot | what it is |
|---|---|
Calendar | State and resolved style. It draws no ground of its own |
Calendar.Header | The row above the grid, and the glyph scale in it |
Calendar.Title | The month, named through the locale |
Calendar.PreviousButton | A month back. Dead at the bounds |
Calendar.NextButton | A month forward |
Calendar.Weekdays | The seven column headings |
Calendar.Grid | Six weeks of cells |
Calendar.Day | One day |
Calendar.YearPicker | The years a calendar can be aimed at |
Calendar.MonthPicker | The twelve months of the year just aimed at |
The header lays itself out with space-between and nothing else. Where the title sits —
between the chevrons, or beside them with both on the right, as AgendaCalendar has it — is
what the caller writes, not a prop.
Usage
import { Calendar } from '@xaui/native/calendar'
Root props
CalendarProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | CalendarVariant | undefined | No | — |
| size | CalendarSize | undefined | No | The cell's box and its type. The grid spans its parent either way. |
| radius | RadiusKey | undefined | No | The chosen day's corner, which is a circle by default. |
| color | string | undefined | No | A raw tint (`'#7c3aed'`), never a token (R7). It lands on the chosen day. |
| value | Date | undefined | No | The chosen day. Controlled — leave it out and the calendar holds its own. |
| defaultValue | Date | undefined | No | The day chosen at first mount. |
| onValueChange | ((value: Date) => void) | undefined | No | Fired with the day that was pressed, at midnight local time. |
| month | Date | undefined | No | The month on screen. Controlled **separately from the value**, because paging through months is not choosing a day: a calendar that jumped back to the chosen month every time you looked at the next one would be unusable, and one that chose a day because you paged past it would be worse. |
| defaultMonth | Date | undefined | No | The month shown at first mount. Defaults to the chosen day's, or to this one. |
| onMonthChange | ((month: Date) => void) | undefined | No | — |
| minValue | Date | undefined | No | The earliest day that can be chosen. Days before it are dimmed and inert. |
| maxValue | Date | undefined | No | The latest day that can be chosen. |
| firstDayOfWeek | WeekDay | undefined | No | Which day a week starts on. Read from `locale` when unset — `Intl` answers it properly where it exists, and a list of Monday-first languages is the fallback. |
| locale | string | undefined | No | Names the months and the weekdays. |
| view | CalendarView | undefined | No | Which panel is on screen — the days, the years, or the months. Controlled **separately from the month and the value**, the same way they are separate from each other: the picker components step it, and the caller reads it to render one of the three. |
| defaultView | CalendarView | undefined | No | The panel shown at first mount. |
| onViewChange | ((view: CalendarView) => void) | undefined | No | — |
| isDisabled | boolean | undefined | No | Dims the calendar and stops every day. |
| style | StyleProp<ViewStyle> | No | — |
| children | ReactNode | No | — |
Slots
CalendarDayProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| date | Date | Yes | Which day this cell is. Everything else about it is read off the calendar. |
| children | ReactNode | No | Replaces the number and the dot. |
| isDisabled | boolean | undefined | No | Stops this one day, whatever the bounds say. |
| style | StyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>) | No | R9 — `Pressable`'s function form as much as an object or an array. |
CalendarGridProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | ((date: Date) => ReactNode) | No | A **function**, and the one place in this library that takes one: forty-two cells are generated from a month rather than written, so there is nothing for a caller to compose against. Given one, it renders each day; given nothing, it renders the built-in day. |
CalendarMonthPickerProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| format | "long" | "short" | undefined | No | How the month names read. `'long'` is "septembre", `'short'` is "sept." — the grid clips either to one line rather than reflowing, so a long name in a narrow column is a truncation, not a broken row. |
| children | ReactNode | No | — |
CalendarTextSlotProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
CalendarViewSlotProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
CalendarYearPickerProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| firstYear | number | undefined | No | First year on the list. Defaults to fifty back — from `minValue`'s year when the calendar has one. |
| lastYear | number | undefined | No | Last year on the list. Defaults to fifty forward — from `maxValue`'s year when the calendar has one. |
Variants, sizes and colour
Sizes
size | Cell | Number | Title | Weekday |
|---|---|---|---|---|
sm | 36 | 14/20 | 16/24 | 12/16 |
md | 40 | 16/24 | 18/28 | 14/20 |
lg | 44 | 18/28 | 20/30 | 16/24 |
size is the cell's box, never the grid's width. A calendar is seven columns wide
whatever the size, so a size that drove width would be deciding the calendar's own; the
grid spans its parent and each column takes a seventh of it.
radius is the circle size sets, and it moves the cell and the disc together — a
squared-off day holding a round disc is what the pair exists to prevent.
Variants and colour
variant | The chosen day | Its number |
|---|---|---|
primary | accent | accentForeground |
secondary | accentSoft | accentSoftForeground |
tertiary | default | defaultForeground |
ghost | foreground | background |
Four emphasis levels and no intent — a date is neither a success nor a danger. What the variant names is the chosen day, because a day at rest is the same on all four: a calendar is a grid of numbers, and the variant is about the one that is answered.
The pair is bgSelected / fgSelected — the Checkbox's roles, for the Checkbox's
reason. Forty-two cells share one resolution and exactly one is chosen, so selection cannot
be a variant axis without resolving the recipe per cell; and it has to be a role or a raw
color would stop reaching the chosen day the moment it became the chosen one, since the
tint pass re-runs paint and never the axes.
color is a raw value (R7) and lands on the disc, with the number derived to read against it.
Accessibility
- Each day is a
buttonlabelled with its full date — "dimanche 6 septembre 2026" — because the number alone says "6", and carriesaccessibilityState.selected. - The title is a
header, which is what a screen reader jumps between. - The weekday row is hidden from the accessibility tree: the names are for the eye, and each day already announces the day it is.
- The chevrons need an
accessibilityLabelfrom the caller: the month they go to is what should be announced, and only the caller knows which language to say it in.
Migration from legacy
Implementation notes
The month on screen is state of its own
<Calendar
value={date}
onValueChange={setDate}
month={month}
onMonthChange={setMonth}
/>
Paging through months is not choosing a day. A calendar that jumped back to the chosen month every time you looked at the next one would be unusable, and one that chose a day because you paged past it would be worse. So there are two pieces of state, and either can be controlled without the other.
Uncontrolled, the month starts on the chosen day's — or on this one.
The grid is always six weeks
Never five for a short month. A grid that changed height between March and April would move everything under it twice a year, and the calendar would jump as you paged through it.
Days from the months either side fill the ends. They are muted and still choosable: a
calendar that refused the 1st of next month would be refusing a date you can see. What makes
a day inert is minValue / maxValue, not which month it belongs to.
The week starts where the locale says
Intl.Locale's week info answers it properly where it exists — Saturday-first locales are
real, and a hand-kept list of Monday-first languages has never included them. Most React
Native engines do not have it, so a list of Monday-first languages is the fallback rather
than the source.
firstDayOfWeek={1} overrides both. locale names the months and the weekdays, and defaults
to the device's.
Cells of your own
<Calendar.Grid>
{date => (
<Calendar.Day key={date.getTime()} date={date}>
<Text>{date.getDate()}</Text>
</Calendar.Day>
)}
</Calendar.Grid>
Calendar.Grid takes a function, and it is the one place in this library that does.
Forty-two cells are generated from a month rather than written, so there is nothing for a
caller to compose against: asChild merges into one element, and a slot list cannot
enumerate a month.
A day is a date plus the calendar around it. Calendar.Day reads everything else off
its date — chosen, outside the month, out of bounds, today — which is what makes the
function above two lines rather than a wiring exercise. It is also what AgendaCalendar
builds its event dots on.
The bounds
<Calendar minValue={new Date()} maxValue={addMonths(new Date(), 3)} />
Days outside them are muted and inert, and the chevrons go dead: a step that would land on a month with no selectable day in it has nothing to show, and a chevron that stays lit while it stops working is the worst of the three options.
Bounds are compared by day, not by instant. A maxValue written as new Date() carries
the current time, and an instant comparison would refuse the rest of today.
Today
Today carries a small dot tucked just under its number — the soft accent on a plain day, and the chosen day's own contrast colour when today is the chosen day. It is positioned absolutely so the number does not shift down the day the dot appears.
The dates behind it
The arithmetic is utils/dates.ts, and it is tested: startOfDay, isSameDay,
isSameMonth, addDays, addMonths, startOfWeek, isWithinBounds, monthGrid,
weekGrid, weekdayNames, monthLabel, monthNames, firstDayOfWeekFor.
Two of those exist because the obvious version is wrong:
addMonthsclamps to the end of the target month.new Date(2026, 0, 31)plus a month is the 31st of February, whichDaterolls forward to the 3rd of March — so a calendar stepping from January to February would land on March.addDaysgoes through the day-of-month, not through milliseconds. A day is not always 86 400 seconds, and adding that many across a daylight-saving boundary lands an hour into the day before.
Switching the month and the year
Calendar.YearPicker and Calendar.MonthPicker are the legacy dialog's month-year button
rebuilt in composition. Each mounts instead of the weekdays and the grid, not over them
— a drum of years behind a drum of days would read as two things at once — and the walk is
legacy's: the title opens the years, a year opens the months of that year, a month lands
back on the days.
view is the third piece of state, controlled the way value and month are. The pickers
step it themselves; the caller owns it so the title can be the switch:
const [view, setView] = useState<CalendarView>('grid')
<Calendar value={date} onValueChange={setDate} view={view} onViewChange={setView}>
<Calendar.Header>
<Calendar.PreviousButton accessibilityLabel="Mois précédent" />
<PressableFeedback onPress={() => setView(v => (v === 'grid' ? 'year' : 'grid'))}>
<Calendar.Title />
</PressableFeedback>
<Calendar.NextButton accessibilityLabel="Mois suivant" />
</Calendar.Header>
{view === 'year' ? (
<Calendar.YearPicker />
) : view === 'month' ? (
<Calendar.MonthPicker />
) : (
<>
<Calendar.Weekdays />
<Calendar.Grid />
</>
)}
</Calendar>
The year and the month on screen each wear the chosen day's disc, and the year list opens on the year it was on. Pressing a year keeps the month of the year it was showing, and pressing a month keeps the year — a header that said "septembre 2026" has not become a switch to January. A year or a month that lies entirely outside the bounds is dead rather than merely inert.
The caller reads view to choose which of the three to render, so in practice it holds the
state — as above, or from a child on useCalendar(). The pickers only ever step it.
Extending it
useCalendar() is exported (R10) and carries the resolved styles, the month on screen, the
chosen day, the locale, the panel (view / setView), and the moves: select,
goToMonth, canGoToMonth, goToYear and goToMonthInYear. Enough for a "Today" button
of your own, or a title on useCalendar().setView — the pickers the built-in ones serve run
on nothing else. Outside a <Calendar> it throws by name.