Calendar
Single, range and multiple selection with full keyboard grid navigation.
Built on Icons.
- Check-in
- Fri, Dec 11
- Check-out
- Tue, Dec 15
4nights
$186 × 4 nights$744
Installation
npx shadcn@latest add https://neonkit.heyaadi.com/r/calendar.jsonUsage
import { Calendar, type DateRange } from "@/components/ui/calendar"
const [range, setRange] = React.useState<DateRange>({})
<Calendar mode="range" numberOfMonths={2} selected={range} onSelect={setRange} />Examples
A single day
The default mode. Weekends are disabled with a function, so the keyboard skips them too.
Several days
Multiple mode toggles each day on and off and reports them sorted.
Locales
Month and weekday names, digits and the first day of the week all come from Intl.
API reference
Calendar
Also accepts native div attributes, and the ref points at the root. It has no surface of its own, so it sits cleanly in a popover or card.
| Prop | Type | Default |
|---|---|---|
mode | "single"What a click selects. It also sets the types of selected and onSelect. | "single" |
selected | DateControlled selection, shaped by mode. Pass null for controlled with nothing picked. | None |
defaultSelected | DateInitial selection when uncontrolled. | None |
onSelect | (value) => voidSingle: the picked Date. Range: { from } on the first pick, then { from, to }. Multiple: every selected Date, sorted. Dates are local midnight. | None |
month | DateControlled first visible month. | None |
defaultMonth | DateFirst visible month at mount. Falls back to the selection's month, then today's. | None |
onMonthChange | (month: Date) => voidCalled with the first of the month when the view pages, from the buttons or the keyboard. | None |
numberOfMonths | numberMonths shown side by side. They stack below the sm breakpoint. | 1 |
weekStartsOn | 0First column, 0 for Sunday. Defaults to the locale's first day of the week. | from locale |
minDate | DateEarlier days are disabled, and the previous button stops at this month. | None |
maxDate | DateLater days are disabled, and the next button stops at this month. | None |
disabled | (date: Date) => booleanReturn true for days that can't be picked. A range can't run across one. | None |
showOutsideDays | booleanFill the first and last rows with days from the neighbouring months. | true |
locale | stringBCP 47 tag for month and weekday names, digits and the week start. | "en-US" |
autoFocus | booleanFocus the active day on mount and mark it with data-autofocus for a surrounding popover. | false |
color | NeonColorPalette name or hex. Falls back to --neon-accent, then cyan. | inherited |
DateRange
| Prop | Type | Default |
|---|---|---|
from | DateFirst day, inclusive. | None |
to | DateLast day, inclusive. Unset while the end is still being picked. | None |
Accessibility
- Each month is a grid labelled by its caption, and the caption is a polite live region so paging is announced.
- Only one day is in the Tab order: the focused day, else the selection, else today, else the first day that can be picked.
- Arrow keys move by day and week, Page Up and Page Down by month, Shift with them by year, and Home and End to the ends of the week. Enter or Space selects.
- Moving past the visible months pages the view, and moves that land on a disabled day skip ahead to the next one that isn't.
- Days carry a full spoken date, aria-selected, aria-disabled, and aria-current="date" for today.
- A month that has fewer days than the one you came from lands on its last day: Jan 31 plus a month is Feb 28.
- Date math uses local calendar days, never milliseconds, so daylight-saving changes can't skip or repeat a day.
- Today is read on the client only, so server and browser time zones can't disagree during hydration.
- Month slides and the range ignition are skipped with reduced motion.