Skip to Content
ComponentsDatePicker

DatePicker

A trigger-and-popover date picker built on Base UI’s Popover primitive and the Calendar component. Selecting a date closes the popover, or with a controlled open, asks you to close it through onOpenChange (see Open State).

Import

import { DatePicker } from "chunks-ui";

Basic Usage

<DatePicker />

Controlled

function ControlledDatePicker() { const [date, setDate] = useState<Date | null>(null); return ( <DatePicker value={date} onValueChange={setDate} placeholder="Select a date" /> ); }

Date Constraints

Use min and max to limit the selectable range. Dates outside the range are rendered disabled in the calendar:

const today = new Date(); const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate()); <DatePicker min={today} max={nextMonth} />

Week Start and Outside Days

weekStartsOn and showOutsideDays are passed to the calendar in the popover. See Calendar for what they do.

<DatePicker weekStartsOn={1} showOutsideDays />

Shown Month

month and onMonthChange are passed to the calendar too. See Calendar for what they do. With month, the calendar opens on that month, not on the selected date’s, so set it when the date changes:

function MonthDatePicker() { const [date, setDate] = useState<Date | null>(null); const [month, setMonth] = useState(() => new Date()); return ( <DatePicker value={date} onValueChange={(next) => { setDate(next); if (next) setMonth(next); }} month={month} onMonthChange={setMonth} /> ); }

Open State

open and onOpenChange control whether the calendar is open, with the names and the onOpenChange(open, eventDetails) signature of Base UI’s Popover.Root, and defaultOpen opens it on mount. A trigger press, a pick, Escape, an outside press and focus leaving the calendar (focus-out) call onOpenChange with Base UI’s event details; a pick closes the calendar with the reason close-press. A controlled calendar opens and closes only when you change open. Without open, eventDetails.cancel() keeps the calendar as it is. However the calendar opened, a pick or Escape that closes it returns focus to the trigger if focus is still in the calendar.

function OpenDatePicker() { const [open, setOpen] = useState(false); return ( <> <Button onClick={() => setOpen(true)}>Set due date</Button> <DatePicker aria-label="Due date" open={open} onOpenChange={setOpen} /> </> ); }

Disabled

A disabled DatePicker is dimmed and shows the not-allowed cursor. The disabled prop disables it, and so does a disabled Field.Root, Base UI’s Fieldset or a <fieldset disabled>. A disabled picker stays closed whatever open and defaultOpen say, and it ignores a pick. Disabling a picker with an open calendar closes the calendar without calling onOpenChange. When it is enabled again, a controlled calendar opens if open is true; an uncontrolled one stays closed.

The disabled prop does all this at once. The other causes take effect when the picker next renders, because the picker reads the disabled state of its trigger after each render. Until then an open calendar stays open, but a pick in it is already ignored, and closes the calendar. When a disabled picker’s calendar closes with focus in it, focus moves to the page, as it does when any focused control is disabled.

<DatePicker disabled />

Labelling

The trigger is a button. Without a label, its accessible name is its text: the placeholder or the selected date. Every <button> prop that DatePicker does not use itself goes to the trigger: id, aria-label, aria-labelledby, aria-describedby, data-*, event handlers and ref. Use one of these to label it.

With aria-label:

<DatePicker aria-label="Due date" />

With a <label>: give the picker an id and set htmlFor on the label to the same value.

<label htmlFor="due">Due date</label> <DatePicker id="due" />

Inside Field.Root: Field.Label names the trigger and Field.Description describes it.

<Field.Root> <Field.Label>Due date</Field.Label> <DatePicker /> <Field.Description>The task is overdue after this day.</Field.Description> </Field.Root>

The task is overdue after this day.

A click on a <label> or a Field.Label clicks the trigger, so it opens the calendar.

The calendar opens in a dialog named “Choose date”. Set labels.dialog to change the name, for example to translate it.

Forms

Give the picker a name to send the selected date with a <form>. A hidden input holds the date as yyyy-mm-dd, or an empty string when no date is selected. The year always has 4 digits: January 5, 999 is 0999-01-05. A disabled picker sends nothing.

To turn the value back into a local Date, add a time: new Date(`${value}T00:00`). A date and time without an offset is read as local time. Do not use new Date("2026-10-10"): a date alone is read as midnight UTC, which is the day before in time zones west of UTC.

With required, the form does not submit until a date is selected, and focus moves to the trigger. A button has no required state for screen readers, so put it in the label, for example “Due date (required)”. When Base UI’s Form (@base-ui/react/form) validates the field on submit, an empty required picker is valueMissing in Field.Validity: Field.Error with match="valueMissing" shows its message and the trigger gets aria-invalid.

<form onSubmit={(event) => { event.preventDefault(); new FormData(event.currentTarget).get("due"); // "2026-10-10" }} > <Field.Root> <Field.Label>Due date (required)</Field.Label> <DatePicker name="due" required /> </Field.Root> <Button type="submit">Save</Button> </form>

Inside Field.Root, the field’s name wins over the picker’s, as it does for Base UI’s Field.Control. For example, with <Field.Root name="due"> around <DatePicker name="deadline" />, the picker submits its date as due, and in development it warns once, naming both. A picker whose Field.Root has no name submits under its own name. A disabled field disables the hidden input too. For a picker outside its <form>, set form to the form’s id.

Inside Field.Root in Base UI’s Form, an entry for the field’s name in the form’s errors, for example errors={{ due: "Pick a later date" }}, shows in Field.Error and sets aria-invalid on the trigger until the user picks a date. onFormSubmit gets the date under the same name: { due: "2026-10-10" }.

A form reset sets the picker back to its defaultValue, or to no date. This includes a picker that form links to a form it is outside of, with or without a name. A controlled picker keeps its value, as a controlled input does: set it back in the form’s onReset. A reset that onReset cancels with event.preventDefault() leaves the picker as it is. As with native controls, a reset does not call onValueChange: an uncontrolled picker that you mirror in state through onValueChange is out of date after a reset, so reset that state in the form’s onReset too.

When the picker has a name or is required, or its Field.Root has a name, the form input is the field’s control and takes part in the browser’s form validation, as an Input does. A failing validate on Field.Root with validationMode set to "onChange" or "onBlur" then also blocks the submit of a plain <form> and shows the browser’s validation bubble.

Text Size

The text of the trigger has the size of the text fields: 16px below the md breakpoint (768px by default) and on a touch screen, and 14px otherwise, so a form has one text size. To change it, pass a text-size class in triggerClassName. See Text Size in Input.

Keyboard

KeyAction
Enter / Space on the triggerOpens the calendar
Enter / Space on a daySelects the day, closes the calendar and returns focus to the trigger
EscapeCloses the calendar without changing the date and returns focus to the trigger

With a controlled open, these keys only call onOpenChange (a day is still selected at once): the calendar opens or closes, and focus moves, once you change open.

When the calendar opens, focus goes to the selected day, or to today if no date is selected. Inside the calendar, the arrow, Home, End and Page keys move focus as described in Calendar keyboard. Disabled days can take focus but cannot be selected.

DatePicker Props

NameTypeDefault
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 6

First day of the week: 0 = Sunday, 1 = Monday, up to 6 = Saturday.

0
showOutsideDaysboolean

Fill leading and trailing cells with dimmed days from the neighbouring months.

false
monthDate

The shown month (controlled): any date in it. The arrows, the keys and a click on an outside day then call onMonthChange and wait for a new month, and a new value does not move the view. An Invalid Date, or one before year 1, shows the selected day’s month, else today’s. Without it, the calendar opens on the selected day’s month, else today’s, and moves by itself.

onMonthChange(month: Date) => void

Called with the first day of a month, at local midnight, when the user moves to that month: the previous or next month arrow, a key that moves focus out of the shown month, or a click on an outside day. A new value that moves the view does not call it.

formstring

The id of the <form> the picker belongs to, for a picker placed outside it. A reset of that form restores the picker. With name, required or a named Field.Root, the date is submitted with that form.

valueDate | null

The currently selected date (controlled). An Invalid Date counts as no date.

defaultValueDate | null

The initial selected date when uncontrolled. A form reset restores it. An Invalid Date counts as no date.

null
onValueChange(date: Date | null) => void

Called with the picked date, at local midnight, when the user picks an enabled day (click, Enter or Space). Picking the selected day again also calls it. The pick then asks to close the calendar: an uncontrolled one closes unless onOpenChange cancels it, a controlled one when you set open to false. It is never called with null: the picker cannot clear a date. A change to value from outside does not call it.

openboolean

Whether the calendar is open (controlled). An open or close by the user then calls onOpenChange and waits for a new open.

defaultOpenboolean

Whether the calendar is open on mount, when uncontrolled.

false
onOpenChange(open: boolean, eventDetails: PopoverRootChangeEventDetails) => void

Called when the user opens or closes the calendar, with the event details of Base UI’s Popover.Root. A pick closes it with the reason close-press, as a press on Popover.Close does. eventDetails.cancel() keeps an uncontrolled calendar as it is. A change to open from outside does not call it, and neither does the close that disabling the picker forces (see Disabled in the docs). Do not call eventDetails.preventUnmountOnClose(): the picker has no actionsRef to unmount the calendar after it.

placeholderstring

Placeholder text shown in the trigger when no date is selected. Takes precedence over labels.placeholder.

"Pick a date"
disabledboolean

Whether the date picker trigger is disabled. A disabled picker stays closed whatever open and defaultOpen say.

minDate

The earliest selectable date.

maxDate

The latest selectable date.

isDateDisabled(date: Date) => boolean

Predicate function to disable individual dates in the calendar.

namestring

Name of the form field. Renders a hidden input that submits the selected date as yyyy-mm-dd, or an empty string when no date is selected. A name on a surrounding Field.Root wins over this one, as with Base UI’s Field.Control: the form data, and a Base UI Form’s errors and onFormSubmit, then use the field’s name.

requiredboolean

Whether a date must be selected before the form can be submitted. An empty required picker blocks submission and takes focus.

classNamestring

Classes for the wrapper element around the trigger. The trigger fills it, so set the width here. To style the trigger, use triggerClassName.

triggerClassNamestring

Classes for the trigger <button>. They merge after the trigger’s own classes, so a conflicting class replaces the default: h-12 sets the height, and a text-* colour also replaces the muted colour of the placeholder. A text size applies with a mouse from md up: below md and on a touch screen the text stays 16px (see Text Size in the docs).

labelsPartial<DatePickerLabels>

Replaces the built-in English strings, e.g. to translate them, including the calendar’s. Keys you leave out keep the default from DEFAULT_DATE_PICKER_LABELS.

DatePicker also takes every other <button> prop except onChange and passes it to the trigger. Use onValueChange to follow the date.

className goes on the wrapper <div> (set the width here). triggerClassName and style go on the trigger <button>; a conflicting class in triggerClassName replaces the default, such as h-12 for the height. The trigger has a data-placeholder attribute while no date is picked, and the muted colour is a data-placeholder:text-muted-foreground class: a plain text colour in triggerClassName colours the picked date, and data-placeholder:text-red-600 colours the placeholder.

The calendar popup is at least as wide as the trigger, and never narrower than the calendar (about 280px). In a wider popup the calendar sits in the centre.

Labels

labels replaces the built-in English text, for example to translate it. Keys you leave out keep their default from DEFAULT_DATE_PICKER_LABELS. They include every Calendar key, which the picker passes to its calendar. The placeholder prop takes precedence over labels.placeholder. See Labels.

NameTypeDefault
previousMonthstring

Name of the button that shows the previous month.

"Previous month"
nextMonthstring

Name of the button that shows the next month.

"Next month"
heading(date: Date) => string

The heading above the days, which also names the grid. Gets the first day of the shown month, at local midnight.

(date) => "March 2026"
weekday(date: Date) => string

The short weekday name that a column header shows. Gets a date that falls on that weekday, at local midnight.

(date) => "Su" … "Sa"
weekdayLong(date: Date) => string

The full weekday name that screen readers read for a column header, in place of the short one. Gets a date that falls on that weekday, at local midnight. If you set weekday and leave this out, screen readers read your weekday text.

(date) => "Sunday" … "Saturday"
day(date: Date) => string

Accessible name of a day button. Gets its day, at local midnight.

(date) => "Sunday, March 15, 2026"
placeholderstring

Trigger text while no date is selected. The placeholder prop takes precedence.

"Pick a date"
selectedDate(date: Date) => string

Trigger text for the selected date. Gets a picked date at local midnight, and value as you pass it: format it in the local time zone, not with timeZone: "UTC".

(date) => "March 15, 2026"
dialogstring

Accessible name of the dialog that holds the calendar.

"Choose date"
Last updated on