Calendar
A month grid for picking a date — the WAI-ARIA date-picker grid on its own, rather than pre-wired to a popover and an input.
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
Thu Sep 03 2026
Usage#
import { Calendar } from "neelam-ui";
const [selected, setSelected] = useState<Date | undefined>(new Date());
<Calendar selected={selected} onSelect={setSelected} />Composing a date picker#
Calendar deliberately owns no opinion about how it is triggered. Put it in a
Popover behind a button showing the formatted date
and you have the usual date-picker widget:
<Popover>
<PopoverTrigger>{format(selected)}</PopoverTrigger>
<PopoverContent>
<Calendar selected={selected} onSelect={setSelected} />
</PopoverContent>
</Popover>That is the same reasoning
DateRangePicker follows — it is built
from Popover, two Calendars, and a RadioGroup rather than reimplementing
any of them.
Disabling dates#
disabled is a predicate, so any rule works:
<Calendar disabled={(date) => date < new Date()} />Keyboard navigation skips disabled dates rather than landing on them, searching in the direction you were already moving. The search is bounded to a year, so a caller who disables every date cannot hang the component in an infinite loop.
Week start#
weekStartsOn takes 0 (Sunday, the default) through 6. It is not derived
from the locale — that, along with range selection and year-jump shortcuts, is
deliberately out of scope here.
Keyboard#
The grid is fully operable, which is the whole point of the component:
| Key | Behaviour |
|---|---|
| Tab | Enters the grid at the focusable day — one at a time, roving. Tab again leaves the grid entirely. |
| ←→ | Moves one day back or forward. |
| ↑↓ | Moves one week back or forward. |
| HomeEnd | Moves to the first or last day of the current week. |
| PageUpPageDown | Moves to the same day in the previous or next month. |
| EnterSpace | Selects the focused day. |
Accessibility#
- A native
<table role="grid">: rows are weeks, cells are days — a genuinely clean fit for a structure that already is a grid. - Each day is a real
<button>recategorized asrole="gridcell", so activation stays native. Selection isaria-selected, and today carriesaria-current="date". - Roving
tabIndexmeans the whole grid is one tab stop, and real DOM focus is moved by the arrow keys — notaria-activedescendant, matchingSelectandContextMenu. - The month heading is
aria-live="polite", so changing month announces the new one — otherwise paging through months is silent. - The previous/next buttons are icon-only with explicit
aria-labels, and their chevrons arearia-hidden.
Offer a text input too
A grid is excellent for browsing and poor for entering a date you already know. Pairing the calendar with a typed date field is faster for many users, and essential for anyone for whom arrowing across weeks is laborious.
API reference#
| Prop | Type | Default |
|---|---|---|
defaultMonthInitial displayed month when uncontrolled. Defaults to `selected`'s month, or today's. | Date | — |
defaultSelectedInitial selected date when uncontrolled. | Date | — |
disabledMarks specific dates unselectable (e.g. `(date) => date < new Date()` to block the past). | ((date: Date) => boolean) | — |
monthWhich month is shown (any `Date` within it). | Date | — |
onMonthChange | ((month: Date) => void) | — |
onSelectCalled when a day is activated (clicked, or Enter/Space on the focused day). | ((date: Date) => void) | — |
selectedThe selected date. | Date | — |
weekStartsOnWhich weekday starts each row, `0` (Sunday) through `6` (Saturday). Defaults to `0`. | enum | 0 |
Also accepts every native <div> attribute except onSelect, which is taken
over above.