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
| Key | Action |
|---|---|
| Enter / Space on the trigger | Opens the calendar |
| Enter / Space on a day | Selects the day, closes the calendar and returns focus to the trigger |
| Escape | Closes 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
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.