BetaNavigation

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>
slotwhat it is
CalendarState and resolved style. It draws no ground of its own
Calendar.HeaderThe row above the grid, and the glyph scale in it
Calendar.TitleThe month, named through the locale
Calendar.PreviousButtonA month back. Dead at the bounds
Calendar.NextButtonA month forward
Calendar.WeekdaysThe seven column headings
Calendar.GridSix weeks of cells
Calendar.DayOne day
Calendar.YearPickerThe years a calendar can be aimed at
Calendar.MonthPickerThe 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.

PropTypeRequiredDescription
variantCalendarVariant | undefinedNo
sizeCalendarSize | undefinedNoThe cell's box and its type. The grid spans its parent either way.
radiusRadiusKey | undefinedNoThe chosen day's corner, which is a circle by default.
colorstring | undefinedNoA raw tint (`'#7c3aed'`), never a token (R7). It lands on the chosen day.
valueDate | undefinedNoThe chosen day. Controlled — leave it out and the calendar holds its own.
defaultValueDate | undefinedNoThe day chosen at first mount.
onValueChange((value: Date) => void) | undefinedNoFired with the day that was pressed, at midnight local time.
monthDate | undefinedNoThe 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.
defaultMonthDate | undefinedNoThe month shown at first mount. Defaults to the chosen day's, or to this one.
onMonthChange((month: Date) => void) | undefinedNo
minValueDate | undefinedNoThe earliest day that can be chosen. Days before it are dimmed and inert.
maxValueDate | undefinedNoThe latest day that can be chosen.
firstDayOfWeekWeekDay | undefinedNoWhich 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.
localestring | undefinedNoNames the months and the weekdays.
viewCalendarView | undefinedNoWhich 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.
defaultViewCalendarView | undefinedNoThe panel shown at first mount.
onViewChange((view: CalendarView) => void) | undefinedNo
isDisabledboolean | undefinedNoDims the calendar and stops every day.
styleStyleProp<ViewStyle>No
childrenReactNodeNo

Slots

CalendarDayProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
dateDateYesWhich day this cell is. Everything else about it is read off the calendar.
childrenReactNodeNoReplaces the number and the dot.
isDisabledboolean | undefinedNoStops this one day, whatever the bounds say.
styleStyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)NoR9 — `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.

PropTypeRequiredDescription
childrenReactNode | ((date: Date) => ReactNode)NoA **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.

PropTypeRequiredDescription
format"long" | "short" | undefinedNoHow 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.
childrenReactNodeNo

CalendarNavButtonProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNoReplaces the built-in chevron.
stepnumber | undefinedNoHow many months one press moves. Negative goes back.

CalendarTextSlotProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

CalendarViewSlotProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

CalendarYearPickerProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
firstYearnumber | undefinedNoFirst year on the list. Defaults to fifty back — from `minValue`'s year when the calendar has one.
lastYearnumber | undefinedNoLast year on the list. Defaults to fifty forward — from `maxValue`'s year when the calendar has one.

Variants, sizes and colour

Sizes

sizeCellNumberTitleWeekday
sm3614/2016/2412/16
md4016/2418/2814/20
lg4418/2820/3016/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

variantThe chosen dayIts number
primaryaccentaccentForeground
secondaryaccentSoftaccentSoftForeground
tertiarydefaultdefaultForeground
ghostforegroundbackground

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 button labelled with its full date — "dimanche 6 septembre 2026" — because the number alone says "6", and carries accessibilityState.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 accessibilityLabel from 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

This component declares no rule of its own. The migration guide covers variants, colours and slots.

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:

  • addMonths clamps to the end of the target month. new Date(2026, 0, 31) plus a month is the 31st of February, which Date rolls forward to the 3rd of March — so a calendar stepping from January to February would land on March.
  • addDays goes 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.