Skip to content

Calendar

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/calendar.json
See it liveA month, one with bounds, and one showing a range.→

The value is YYYY-MM-DD, never a Date. A birthday has no timezone and a release date has no hour. Put one in a Date and it becomes a moment — and moments cross midnight when they are serialised, which is how a date reaches a server a day early. The string is what a database column holds and what JSON carries.

<Calendar
value={date} // '2026-09-02'
onValueChange={setDate}
previousMonthLabel="Previous month"
nextMonthLabel="Next month"
/>

No date library, deliberately. react-day-picker is good and would bring date-fns and @date-fns/tz behind it — the first heavy dependency in a set that is otherwise Base UI or nothing. Intl already knows the part a library would be consulted for: which day the week starts on here, which days are the weekend, what the months are called. The rest is arithmetic, and it lives in calendar-math with no React in it, so a product doing its own date work can import it alone.

Arrows move, Enter chooses. The grid is one tab stop and the arrows move a cursor within it — forty-two tab stops is not a control. Only Enter or a click fires onValueChange, so a product listening for it does not receive five dates on the way to the sixth.

Key
← → ↑ ↓ move a day or a week
PageUp PageDown move a month
Home End the ends of the week
Enter Space choose the day under the cursor

Six rows, always. A calendar that changes height between months is a popup that moves under the pointer.

Every day is named in full — “2 September 2026”, not “2”. A number on its own is not a date to anything reading the page aloud.

calendar-math is importable on its own, and every function takes and returns the string:

today() the local day — not toISOString().slice(0, 10), which returns tomorrow for anyone east of Greenwich in the evening
addDays(date, n) crosses months, years and February
addMonths(date, n) clamps: a step back from 31 March lands on 28 February
daysInMonth(y, m) the leap rule in full, century exception included
isIsoDate(value) shape and existence — 2026-02-31 is not a day
weekday(date) 1 is Monday, 7 is Sunday, as Intl numbers them
monthGrid(month, locale) six whole weeks, starting on the locale’s first day

Props

Read from the types in ui/calendar.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

PropTypeDefault
valuestringThe selected day, or undefined for none.
onValueChange(value: string) => voidCalled with the day the reader chose. It fires on Enter and on a click, not on the arrows - those only move the cursor.
monthstringWhich month is shown. Uncontrolled unless given.
onMonthChange(month: string) => voidCalled with the new month when the shown month changes: the paging buttons, or the cursor arrowing off the end of a month.
minstringBounds, inclusive. A day outside them cannot be chosen.
maxstringThe latest day that can be chosen, inclusive. See min.
rangeEndstringFor a range: the other end, so the days between can be shaded.
localestringFormats the names. The application's language by default - see useLocale.
aria-labelstringWhat the grid is called, for a screen reader.
autoFocusbooleanfalsePut the focus on the day under the cursor when it mounts - for a calendar that opens in a popup, where choosing a day is what the reader came for. Left to the popup, the focus went to the first thing in it: the month-paging button, two Tabs away from the days.
previousMonthLabelrequiredstringNames the buttons that page the months. Required: they are icons, and an icon with no name is a button that announces nothing.
nextMonthLabelrequiredstringNames the button that pages to the next month. Required, for the same reason as previousMonthLabel.
classNamestring

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 21 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/calendar.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.40.0
    • refactor say state on the element, and measure the palette's anchor apart 8d9b4e1
    • fix put the calendar's cursor on the focus, and open a picker on the day fe23f69
  3. v0.34.0
    • feat the weeks a month touches, and one key map 7658a0c
  4. v0.33.0
    • feat one caption utility and the compact rail width 88a3fee
  5. v0.31.0breaking
    • feat dates and numbers in the application's language 9be5f95
  6. v0.18.1
    • feat dates, on Intl rather than on a date library 5ec8403