Components / Structure
Calendar
A month of days to pick one from, or to lay out a month's plans on.
When to useLink to "When to use"
- Choosing a day where seeing the month helps (a booking, a due date).
- A month view of plans or events:
renderDayputs your content in each day,markDaytints days and adds dots.
Use something else when:
- the date goes into a form field: DateField, which types or picks
- people pick a time of day: TimeField
UsageLink to "Usage"
import { Calendar } from "@arshad-shah/cynosure";
<Calendar value={date} onChange={setDate} min="2026-01-01" isDateDisabled={isFullyBooked} />Dates are YYYY-MM-DD strings: no time and no time zone. Month and day names come from Intl in the locale you give (the browser's by default), and the week starts where that locale starts it.
Limits and unavailable daysLink to "Limits and unavailable days"
min and max stop focus and the month buttons. isDateDisabled marks days that can be focused (so people can hear about them) but not chosen.
A month of plansLink to "A month of plans"
renderDay(date, state) adds your content under each day's number; the grid then fills its container. onMonthChange tells you which month to load.
With renderDay a day is at least --cy-calendar-content-size tall: 2.5 times the cell size by default, and 66px on touch screens (1.5 times the 44px target), so a six-week month is under 520px tall in a 390px-wide container. Set the token for taller or shorter days; a day never drops below the touch target.
Marked daysLink to "Marked days"
markDay(date, state) marks a day without drawing it. Return any of:
tone: tints the day softly with that tone. Days of other months are not tinted; the selected day keeps its fill and today its ring.dots: one small dot per entry under the number, each a tone or any CSS colour. Three at most are drawn; further entries are dropped, so put the count inlabel.label: words added to the day's name after its date, so the marks are not colour alone.
state is { selected, today, outside, disabled, weekend }. weekend follows the locale where the browser knows its weekend, and is Saturday and Sunday otherwise.
<Calendar
markDay={(date, day) => ({
tone: day.weekend ? "neutral" : undefined,
dots: tasks[date]?.map((task) => task.colour),
label: tasks[date] ? `${tasks[date].length} tasks` : undefined,
})}
/>The month shownLink to "The month shown"
Left alone, the calendar starts on defaultMonth (or the month of the value or focused date), moves itself, and follows a new value to its month.
Pass month (YYYY-MM, or any YYYY-MM-DD date of that month) to own it. Every way of leaving the month then calls onMonthChange with the first day of the month asked for, and the grid waits for the new month. If you do not move, focus stays on the day it was on. A new value in another month does not move a controlled month: set month too.
const [month, setMonth] = useState("2026-11");
<Calendar month={month} onMonthChange={setMonth} />Jumping to a month and yearLink to "Jumping to a month and year"
monthPicker turns the heading into a button that opens a panel: a year to step through and its twelve months. Months and years outside min and max are disabled. It is off by default.
PropsLink to "Props"
| Prop | What it does | Type | Default |
|---|---|---|---|
value | The selected date, YYYY-MM-DD, or null for none. | string | null | |
defaultValue | The date selected at first, when uncontrolled. | string | null | |
onChange | Called when a day is chosen, with its date as YYYY-MM-DD. | ((date: string) => void) | |
defaultFocusedDate | The date focus starts on when nothing is selected. Default: today. | string | today |
month | The month shown, controlled: YYYY-MM, or any YYYY-MM-DD date of that month. The grid then only moves when this changes: paging, the month picker, keys that leave the month and choosing a day of another month call onMonthChange and wait for it. A new value in another month does not move a controlled month. | string | |
defaultMonth | The month shown at first, when uncontrolled (YYYY-MM or YYYY-MM-DD). Default: the month of the selected or focused date. | string | |
onMonthChange | Called with the first day of a month (YYYY-MM-01) when the month shown changes (to load that month's events) or, with month, when another month is asked for. | ((firstDay: string) => void) | |
monthPicker | Turns the month heading into a button that opens a panel to jump to any month and year. Months and years outside min and max cannot be chosen. Default false. | boolean | false |
min | Earliest and latest dates that can be reached and chosen. | string | |
max | Latest date (YYYY-MM-DD) that can be reached and chosen. | string | |
isDateDisabled | Dates that can be focused but not chosen (fully booked, weekends). | ((date: string) => boolean) | |
locale | Locale for month and day names (and the first day of the week). Default: the browser's. | string | the browser's |
weekStartsOn | 0 Sunday to 6 Saturday. Default: the locale's first day, else Monday. | number | the locale's first day |
today | What counts as today. Default: the device's date. | string | the device's date |
renderDay | Extra content inside a day, under its number: a dot for events, a planner's items. | ((date: string, state: CalendarDayState) => ReactNode) | |
markDay | Marks a day without drawing it: return { tone, dots, label } to tint its background, put up to three dots under its number and add words to its name, or nothing to leave it plain. Weekends: (_, day) => day.weekend ? { tone: "neutral", label: "Weekend" } : undefined. | ((date: string, state: CalendarDayState) => CalendarDayMark | undefined) | |
labels | Replace the built-in English text: the month buttons, and the month picker's name and year buttons. | Partial<CalendarLabels> | |
size | Size of the day cells and text. Default "md". | Size | "md" |
className | Added to the root element, for layout. | string | |
style | Layout only. Restyle through --cy-calendar-* tokens. | CSSProperties |
Generated from the type definitions in @arshad-shah/cynosure 0.5.0.
Behaviour and accessibilityLink to "Behaviour and accessibility"
| Key | Does |
|---|---|
| Left / Right | Previous / next day (flipped in right-to-left text) |
| Up / Down | Same day of the previous / next week |
| Home / End | First / last day of the week |
| Page Up / Page Down | Same day of the previous / next month (Shift: year) |
| Enter, Space | Choose the focused day |
- Roles: a
gridnamed by the month title, column headers with the full day name asabbr, and one Tab stop on the focused day. - Names: each day reads as its full date, then any
markDaylabel, then anyrenderDaycontent; today isaria-current="date"; unavailable days arearia-disabled. - Announcements: the month title is a polite live region.
- Focus: the focused day is always a day of the month shown. When the month changes it is the same day number there (or the month's last day), kept within
minandmaxwhere the month has such a day. - Month picker: the heading button is named by the month and "Choose month and year" and opens a dialog. Focus moves into it; Tab moves through the year buttons and the months, Enter or Space chooses a month, closes the panel and puts focus back on the heading. Escape closes it and changes nothing. The month shown is
aria-current. - Forced colours: a tinted day has a dashed outline and dots take the text colour.
- Labels:
labelsreplacesprevious,next,monthPicker,previousYearandnextYear.
TouchLink to "Touch"
Days are at least 44px on touch screens, also with renderDay: a grid in a container narrower than seven targets scrolls. Tap a day to choose it; the month buttons and the month picker's buttons have the same hit area.
ThemingLink to "Theming"
| Token | Sets |
|---|---|
--cy-calendar-cell-size | A day's size (never below the hit area) |
--cy-calendar-selected-background | Background of the selected day |
--cy-calendar-content-size | With renderDay, the least height of a day (never below the hit area) |
--cy-calendar-tone-mix | How much of a marked day's tone is mixed into its background (12%) |
--cy-calendar-dot-size | Size of a day's dots |
The selected day uses the accent and its on-accent text colour.
RelatedLink to "Related"
These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.