DatePicker
A date field: type the date in your locale's order, or pick it from a calendar that opens under the field.
cennge-ui 0.4.0 ships 6 days after the code freeze.
- Type 11192026, 11/19/26 or 2026-11-19, then Tab or Enter: the field writes it out in the locale's format.
- In Release, type a date before the freeze, or 02/30/2026: the text stays, with the reason under it; Escape puts the saved date back.
- Alt + Down or the calendar button opens the calendar on the chosen day; arrow to another day, and Enter picks it and closes the calendar.
Field marks
- Import
import { DatePicker } from "cennge-ui";- Size
- 10.2 KB gzip for this import, its dependencies aside
- Pattern
- Text field, and a calendar in a popover dialog
- Keyboard
- AltArrow down Enter Esc
- State
- Controlled with
valueandonValueChange(an ISO date ornull), or uncontrolled - SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Typing
- The locale's order, any separators or none:
11/19/2026,11.19.26,111926in en-US; ISO in any locale - Form value
- The ISO date, in a hidden input under
name - Tests
- Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: date-picker
When to use#
Use a DatePicker for a date in a form: a release, a due date, a birthday. Some people type dates and others click them, and it takes both. The value is an ISO string with no time zone, "2026-11-19", the same as <input type="date"> gives, so it goes to a server unchanged.
Typed text is read when the field loses focus or on Enter, in the order the locale writes dates and with any separators. Text that is not a date, or a date it cannot take, stays in the field with the reason under it.
Similar#
- Calendar is the same month grid, always open on the page.
- DateRangePicker takes a start and an end in one calendar.
- TimeInput takes the time of day; put the two side by side for a date and a time.
Type it or pick it#
The calendar button opens the calendar on the chosen day, or on today. Picking a day closes it and returns focus to the button. An emptied field sets the value to null.
value: null
import { useState } from "react";
import { DatePicker } from "cennge-ui";
export default function ReleaseDate() {
const [date, setDate] = useState<string | null>(null);
return (
<div className="flex w-full max-w-64 flex-col gap-3">
<DatePicker label="Release date" value={date} onValueChange={setDate} />
<p className="text-sm text-muted">
value: <code className="font-code text-foreground">{date ? `"${date}"` : "null"}</code>
</p>
</div>
);
}Order and separators from the locale#
The placeholder, the way the field writes the date and the order in which it reads typed numbers come from Intl for the locale: month first in the United States, day first in Britain, year first in Japan. Set it once on CenngeProvider, or per field. These three share one value: type in any of them.
import { useState } from "react";
import { DatePicker } from "cennge-ui";
const locales = ["en-US", "en-GB", "ja-JP"];
export default function OneDateThreeLocales() {
const [date, setDate] = useState<string | null>("2026-11-19");
return (
<div className="grid w-full gap-4 sm:grid-cols-3">
{locales.map((locale) => (
<DatePicker key={locale} label={locale} locale={locale} value={date} onValueChange={setDate} />
))}
</div>
);
}Limits counted from today#
minValue and maxValue disable the days outside them in the calendar and turn a typed date outside them down with the reason. isDateUnavailable crosses single days out of the calendar and turns them down when typed. Only the browser knows the reader's today, so read it after mount; until then null means no limit.
import { useEffect, useState } from "react";
import { DatePicker } from "cennge-ui";
const toIso = (date: Date) =>
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
const isWeekend = (iso: string) => {
const day = new Date(`${iso}T00:00:00Z`).getUTCDay();
return day === 0 || day === 6;
};
export default function DueDate() {
// Today is the browser's, so it is read after mount; until then null means no limit.
const [limits, setLimits] = useState<{ min: string; max: string } | null>(null);
useEffect(() => {
const now = new Date();
const inNinetyDays = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 90);
setLimits({ min: toIso(now), max: toIso(inNinetyDays) });
}, []);
return (
<DatePicker
label="Due date"
description="A weekday in the next 90 days"
minValue={limits?.min ?? null}
maxValue={limits?.max ?? null}
isDateUnavailable={isWeekend}
className="w-full max-w-64"
/>
);
}In a form#
With a name, a hidden input carries the ISO date. Enter in the field reads the typed date before the form is submitted, so typing and pressing Enter sends it. Inside a Form, isRequired and errors from your server show under the field.
import { useState } from "react";
import { Button, DatePicker, Form } from "cennge-ui";
export default function Milestone() {
const [due, setDue] = useState<string | null>(null);
return (
<Form
onSubmit={(event) => {
event.preventDefault();
setDue(String(new FormData(event.currentTarget).get("due")));
}}
className="flex w-full max-w-64 flex-col gap-4"
>
<DatePicker name="due" label="Milestone due" isRequired />
<div className="flex flex-wrap items-center gap-3">
<Button type="submit" color="primary">
Save
</Button>
{due ? <code className="font-code text-sm text-muted">due={due}</code> : null}
</div>
</Form>
);
}In another language#
formatError turns each refusal into your text: it receives invalid with the locale's pattern, outOfRange with the limits, or unavailable with the date. labels names the calendar button and the calendar's controls, and placeholder replaces the pattern shown in the empty field.
import { CenngeProvider, DatePicker, type DatePickerError } from "cennge-ui";
const asGerman = (iso?: string) => (iso ? iso.split("-").reverse().join(".") : "");
const errorText = (error: DatePickerError) => {
switch (error.type) {
case "invalid":
return "Datum als TT.MM.JJJJ eingeben";
case "unavailable":
return "An diesem Tag nicht möglich";
case "outOfRange":
return `Datum zwischen ${asGerman(error.min)} und ${asGerman(error.max)} wählen`;
}
};
export default function GermanDatePicker() {
return (
<CenngeProvider locale="de-DE">
<DatePicker
label="Veröffentlichung"
minValue="2026-11-01"
maxValue="2026-12-31"
placeholder="TT.MM.JJJJ"
formatError={errorText}
labels={{
chooseDate: "Datum wählen",
previousMonth: "Vorheriger Monat",
nextMonth: "Nächster Monat",
month: "Monat",
year: "Jahr",
}}
className="w-full max-w-64"
/>
</CenngeProvider>
);
}Accessibility#
The field is a text input with its label, so screen readers announce it as one, and a refusal sets aria-invalid with the reason as its description. The calendar button is named "Choose date", and so is the dialog it opens. The calendar moves focus to the chosen day when it opens. The first Escape closes an open month or year grid, the next one the calendar.
Props#
Also takes every attribute of <input>, except size, color, value, defaultValue, onChange, min, max, type, which mean something else here.