Skip to content

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.

tsx
import { DatePicker } from 'neba';

<DatePicker label="Ships on" placeholder="Pick a day" clearable />;

Props

PropTypeDefaultDescription
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
elevationshared0 | 1 | 2 | 30Drop shadow depth of the trigger. The popup carries its own
valueDate | nullThe chosen day. Use with onValueChange for a controlled picker
defaultValueDate | nullThe initial value, for an uncontrolled picker
onValueChange(value: Date | null) => voidCalled 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
defaultMonthDateWhich month the calendar opens on when there is no value. Defaults to this one
minDateDate | nullThe earliest date that may be chosen, compared at granularity. A minimum of 15 March drops the 14th at day and keeps March at month
maxDateDate | nullThe other end of the same span
shouldDisableDate(date: Date) => booleanBlocks 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
weekStartsOnshared0 | 1 | 2 | 3 | 4 | 5 | 6Which day the week starts on, Sunday being 0. Defaults to whatever the locale says
showTodayButtonbooleantrueOffers the shortcut to the current unit in the footer: today, this month or this year
openbooleanWhether the popup is open. Use with onOpenChange for a controlled one
defaultOpenbooleanfalseWhether it starts open
onOpenChange(open: boolean) => voidCalled when the popup opens or closes
localestringBCP 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
formatIntl.DateTimeFormatOptionsHow the trigger writes the value. Passed straight to Intl
placeholderReactNodeShown in the trigger while nothing is chosen
clearablebooleanfalseOffers the × that empties the control
closeOnSelectbooleantrueCloses the popup as soon as a day is chosen. True by default, because only one thing was asked
labelsPartial<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
namestringIdentifies the field when a form is submitted, written at granularity: YYYY-MM-DD, YYYY-MM or YYYY, local rather than UTC
fullWidthbooleanfalseStretches to the width of the container
labelReactNodeThe label, wired to the control by Base UI's Field
descriptionReactNodeHelper text
errorReactNodeError message. Its presence turns the control invalid and re-points the colour family at danger
invalidboolean!!errorForces the invalid state without a message, for when a form library owns validity
readOnlybooleanfalseShown but not changeable. Keeps its colour and edge, drains the saturation
disabledbooleanfalseUnavailable. 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.

KeyWhat it does
Space / EnterOpens the calendar and focuses the chosen cell
Moves by a day or a week, stepping the month at the edges
Home / EndTo the start or the end of the week
PageUp / PageDownBy a month: with Shift, by a year
EscapeCloses 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" of role="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 carries aria-current="date" and a dot under the number.
  • label becomes the trigger's accessible name, and description and error are wired to it with aria-describedby.
  • The popup is portalled to the end of <body>, with neba-portal on the positioner.

Released under the MIT License