DatePicker
Chooses one day from a calendar popup. The month name and the year each open a grid of their own, so distant dates stay a few clicks away. Set granularity and the picker asks for a whole month or a whole year instead.
import { DatePicker } from 'neba';
<DatePicker label="Ships on" placeholder="Pick a day" clearable />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface. The same shell as a TextField and a Select |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale. A day cell is on the same ladder: 32px at md |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only: never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth of the trigger. The popup carries its own |
| value | Date | null | — | The chosen day. Use with onValueChange for a controlled picker |
| defaultValue | Date | null | — | The initial value, for an uncontrolled picker |
| onValueChange | (value: Date | null) => void | — | Called when the chosen day changes |
| granularity | 'day' | 'month' | 'year' | 'day' | Which unit the picker asks for: a day, a whole month, a whole year. At month and year the calendar opens on that grid and stops there, and the value is the first day of what was chosen. The default format, the footer button, what name submits and the unit the three props below are read at all follow it |
| defaultMonth | Date | — | Which month the calendar opens on when there is no value. Defaults to this one |
| minDate | Date | null | — | The earliest date that may be chosen, compared at granularity. A minimum of 15 March drops the 14th at day and keeps March at month |
| maxDate | Date | null | — | The other end of the same span |
| shouldDisableDate | (date: Date) => boolean | — | Blocks individual cells inside the range: weekends, holidays, a room already booked. The cell stays in the grid, unavailable, and the callback is handed the value that cell would produce |
| weekStartsOnshared | 0 | 1 | 2 | 3 | 4 | 5 | 6 | — | Which day the week starts on, Sunday being 0. Defaults to whatever the locale says |
| showTodayButton | boolean | true | Offers the shortcut to the current unit in the footer: today, this month or this year |
| open | boolean | — | Whether the popup is open. Use with onOpenChange for a controlled one |
| defaultOpen | boolean | false | Whether it starts open |
| onOpenChange | (open: boolean) => void | — | Called when the popup opens or closes |
| locale | string | — | BCP 47 tag deciding the month and weekday names, the order of the header's two buttons, and how the trigger writes the value. Defaults to the browser's |
| format | Intl.DateTimeFormatOptions | — | How the trigger writes the value. Passed straight to Intl |
| placeholder | ReactNode | — | Shown in the trigger while nothing is chosen |
| clearable | boolean | false | Offers the × that empties the control |
| closeOnSelect | boolean | true | Closes the popup as soon as a day is chosen. True by default, because only one thing was asked |
| labels | Partial<PickerLabels> | — | The strings a screen reader hears. One object rather than twenty props, because they are a set: they default to the locale’s wording, and the date names are not among them, those come from Intl |
| name | string | — | Identifies the field when a form is submitted, written at granularity: YYYY-MM-DD, YYYY-MM or YYYY, local rather than UTC |
| fullWidth | boolean | false | Stretches to the width of the container |
| label | ReactNode | — | The label, wired to the control by Base UI's Field |
| description | ReactNode | — | Helper text |
| error | ReactNode | — | Error message. Its presence turns the control invalid and re-points the colour family at danger |
| invalid | boolean | !!error | Forces the invalid state without a message, for when a form library owns validity |
| readOnly | boolean | false | Shown but not changeable. Keeps its colour and edge, drains the saturation |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
Native <div> attributes pass through to the root. Only color, defaultValue and children are excluded, since the table above spells them differently.
value is a Date | null. There is no date library underneath.
Everything is compared on the local calendar day rather than on a UTC timestamp. The hidden input a form submits is a local string too (YYYY-MM-DD for a day), so nothing shifts by a day the way toISOString() would.
Three views
The two buttons in the header each open a different grid.
- The month name: a grid of twelve months.
- The year: a grid of twelve years, with the steppers moving a page at a time.
Choosing a year hands over to the month view. The two buttons are printed in the order the locale writes them. All three views are the same width and height, so switching between them never resizes the popup.
Examples
variant
The same three weights as TextField, drawn on the same shell.
size
A day cell uses the control heights: 32px at md, the same as a Button or TextField of that size.
granularity
granularity says which of the three grids the reader may stop on. At month and year the calendar opens on that grid and a click there is the answer: there is no day view to fall into.
The value stays a Date, normalised to the first day of what was chosen: 1 March for March, 1 January for 2026. Three other things follow it. The trigger's default format becomes { year: 'numeric', month: 'long' } or { year: 'numeric' }; the footer's shortcut says "This month" or "This year"; and name submits YYYY-MM or YYYY. The shape a native <input type="month"> submits, rather than a day nobody chose.
Climbing is unchanged, so a month picker still reaches any month of any year in two clicks.
minDate · maxDate · shouldDisableDate
minDate and maxDate are compared at granularity, against the whole span a cell stands for. At day a maximum of the 27th at 09:00 still leaves the 27th pickable; at month a minimum of 15 March leaves March pickable, since part of March is allowed. Use shouldDisableDate for cells inside the range that still cannot be chosen: it is handed the value that cell would produce, so at month it receives the 1st.
A blocked cell keeps its place in the grid and is marked with aria-disabled rather than the disabled attribute, so it stays on the arrow-key path.
disabled · readOnly · error
showTodayButton and clearable
showTodayButton adds a button in the popup footer that jumps to the current unit: today, this month or this year, whichever granularity is asking for. clearable adds a button on the trigger that empties the value.
Keyboard
The trigger is a button rather than a text input: the date comes from the calendar.
| Key | What it does |
|---|---|
Space / Enter | Opens the calendar and focuses the chosen cell |
← → ↑ ↓ | Moves by a day or a week, stepping the month at the edges |
Home / End | To the start or the end of the week |
PageUp / PageDown | By a month: with Shift, by a year |
Escape | Closes without choosing |
The grid has a single tab stop, so Tab leaves it rather than walking forty-two cells.
Accessibility
- The grid is a
role="grid"ofrole="gridcell"buttons, each named with the full date rather than the bare number. - The chosen cell carries
aria-selected; the current day, month or year carriesaria-current="date"and a dot under the number. labelbecomes the trigger's accessible name, anddescriptionanderrorare wired to it witharia-describedby.- The popup is portalled to the end of
<body>, withneba-portalon the positioner.