Skip to content
Cynosurev0.5.0
SearchStorybookGitHubnpmColour mode: System. Switch to Light

Components / Structure

Calendar

A month of days to pick one from, or to lay out a month's plans on.

Pick aday

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: renderDay puts your content in each day, markDay tints 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"

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

Limits and unavailable days

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.

Amonth of plans

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 in label.
  • 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.

tsx
Copy
<Calendar
  markDay={(date, day) => ({
    tone: day.weekend ? "neutral" : undefined,
    dots: tasks[date]?.map((task) => task.colour),
    label: tasks[date] ? `${tasks[date].length} tasks` : undefined,
  })}
/>
Marked days

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.

tsx
Copy
const [month, setMonth] = useState("2026-11");

<Calendar month={month} onMonthChange={setMonth} />
Controlled month

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.

Jump to amonth and year

PropsLink to "Props"

Every prop of Calendar
PropWhat it doesTypeDefault
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.stringtoday
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.booleanfalse
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.stringthe browser's
weekStartsOn
0 Sunday to 6 Saturday. Default: the locale's first day, else Monday.numberthe locale's first day
today
What counts as today. Default: the device's date.stringthe 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"

Behaviour and accessibility, table 1
KeyDoes
Left / RightPrevious / next day (flipped in right-to-left text)
Up / DownSame day of the previous / next week
Home / EndFirst / last day of the week
Page Up / Page DownSame day of the previous / next month (Shift: year)
Enter, SpaceChoose the focused day
  • Roles: a grid named by the month title, column headers with the full day name as abbr, and one Tab stop on the focused day.
  • Names: each day reads as its full date, then any markDay label, then any renderDay content; today is aria-current="date"; unavailable days are aria-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 min and max where 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: labels replaces previous, next, monthPicker, previousYear and nextYear.

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"

Theming, table 2
TokenSets
--cy-calendar-cell-sizeA day's size (never below the hit area)
--cy-calendar-selected-backgroundBackground of the selected day
--cy-calendar-content-sizeWith renderDay, the least height of a day (never below the hit area)
--cy-calendar-tone-mixHow much of a marked day's tone is mixed into its background (12%)
--cy-calendar-dot-sizeSize of a day's dots

The selected day uses the accent and its on-accent text colour.

DateField · TimeField · Popover

These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.

Getting started
Theming
Icons
Accessibility
Code editors and highlighters
Using Cynosure without React
Build-tool adapters
Components
Accordion
AppShell
AspectRatio
Breadcrumbs
Calendar
Card
Container
Disclosure
Divider
DocumentViewport
Grid
Inline
PageHeader
PanZoomCanvas
ScrollArea
SideNav
SortableList
SplitPane
Stack
StatusBar
Tabs
TitleBar
Toolbar
Tree
VirtualList
ZoomControls
Announcer
Avatar
Badge
CodeBlock
DescriptionList
EmptyState
ErrorBoundary
Field
Heading
IconTile
Image
Kbd
List
LoadingDots
Notice
ProgressBar
Prose
Skeleton
Stat
Steps
Table
Text
Button
Checkbox
Checklist
ColorField
ColorSwatches
Combobox
CompareSlider
CopyButton
DateField
FileDrop
IconButton
KeyValueEditor
Link
NumberField
Pagination
PathField
RadioGroup
SearchField
SegmentedControl
Select
SelectionFrame
SignaturePad
Slider
Switch
TagInput
TextArea
TextField
ThemeToggle
TimeField
ButtonBase
ChoiceBase
DialogBase
FileInputBase
HiddenValue
Icon
InputBase
LinkBase
LiveRegion
TextAreaBase
ChatMessage
Choice
DataGrid
DeviceFrame
HighlightedText
HotkeyField
ListRow
OptionList
OverlayFrame
QRCode
RowActions
Section
SiteFooter
SiteHeader
SkipLink
SortButton
VisuallyHidden
CommandPalette
ContextMenu
Dialog
FloatingToolbar
HoverCard
Menu
Menubar
Popover
ProgressOverlay
Sheet
Toast
Tooltip
Changelog