Date field
StableA date typed in segments in the locale’s order, with a calendar button beside it. Paste "2026-10-21" to fill it.
For filters and forms where people type dates fast. Each segment is a spinbutton: digits move on by themselves once a segment is full, the arrow keys step, and the separator key jumps ahead. onChange fires with an ISO date once all three parts make a real date, and with null when a part is cleared. For dates people know by heart, use Memorable date.
Start and end dates
import { useState } from 'react'import { DateField } from '@rchursin/product/calendar'
/** * Type the date: digits move on by themselves, ↑ ↓ step, and pasting "2026-10-21" fills all three * parts. The calendar button opens the same panel as the date picker. */export default function DateFieldBasic() { const [start, setStart] = useState<string | null>('2026-10-21') const [end, setEnd] = useState<string | null>(null)
return ( <div className="flex w-full max-w-64 flex-col gap-4"> <DateField label="Start date" value={start} onChange={setStart} /> <DateField label="End date" hint="Leave empty for no end." value={end} onChange={setEnd} /> <p className="font-mono text-[11px] text-fg-3"> start: {start ? `"${start}"` : 'null'} · end: {end ? `"${end}"` : 'null'} </p> </div> )}Usage
import { DateField } from '@rchursin/product/calendar'<DateField label="Start date" value={start} onChange={setStart} />Examples
Checking an order
ISO strings compare correctly as text, so “review on or after start” is one comparison. The first field has no calendar button.
import { useState } from 'react'import { DateField } from '@rchursin/product/calendar'
/** ISO dates compare correctly as plain text, so checking an order is one comparison. */export default function DateFieldValidation() { const [start, setStart] = useState<string | null>('2026-10-21') const [review, setReview] = useState<string | null>('2026-10-19') const tooEarly = Boolean(start && review && review < start)
return ( <div className="flex w-full max-w-64 flex-col gap-4"> <DateField label="Trial starts" value={start} onChange={setStart} picker={false} /> <DateField label="Review by" value={review} onChange={setReview} min={start ?? undefined} error={tooEarly ? 'Review date must be on or after the start date' : undefined} /> </div> )}API
Date field props
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | – | Visible label. It names the group of segments and the calendar button. |
valuerequired | ISODate | null | – | The date as "YYYY-MM-DD", or null. A new value from outside replaces what is typed. |
onChangerequired | (value: ISODate | null) => void | – | Called with an ISO date once day, month and year make a real date, and with null when one is cleared. |
min | ISODate | – | Limits the calendar panel. Typed dates are not checked against it: validate and pass error. |
max | ISODate | – | Limits the calendar panel (typed dates are not checked). |
isDateUnavailable | (date: ISODate) => boolean | – | Dates the calendar panel won’t let you choose. |
hint | string | – | Help text under the label, linked with aria-describedby. |
error | string | – | Error message. Sets aria-invalid on each segment. |
picker | boolean | true | The calendar button beside the segments. |
hideLabel | boolean | – | Keep the label for screen readers only. |
name | string | – | Submit the ISO value (or an empty string) with a form under this name. |
className | string | – | Classes on the wrapper. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| 0123456789 | Types into the segment and moves on once it can’t take another digit. |
| Up arrowDown arrow | Steps the segment by one and wraps (Dec → Jan). On an empty segment, starts at today’s value. |
| Left arrowRight arrow | Previous or next segment. |
| HomeEnd | First or last value: day 1 or the month’s last day, month 1 or 12, year 1900 or 2100. |
| Backspace | Erases the last digit; on an empty segment, moves back to the one before. |
| Del | Clears the segment. |
| /.- | Jumps to the next segment, like typing the separator. |
| Tab | Each segment is a Tab stop, then the calendar button. |
| TabShiftTab | In the calendar dialog: moves between its buttons and the grid and wraps around, so focus stays in it until a choice, Esc or a click outside. |
| CtrlV | Pasting "2026-10-21", "21 Oct 2026" or "October 21, 2026" fills all three. Numbers-only dates like 2/3/2026 are refused: nobody can tell which is the month. |
- The segments sit in a group named by the label. Each is a spinbutton named Day, Month or Year, with its value, range and a spoken text (“10, October”, or “Empty”).
- A two-digit year becomes 20xx or 19xx when you leave it (within 20 years ahead, else last century).
- Phones get the number pad (inputmode="numeric"); digits from touch keyboards are read from beforeinput, so nothing stray lands in a segment.
- The calendar button is named “<label>: choose from a calendar” and opens the same dialog as the date picker.
Guidelines
Do
- Use it where people type dates quickly: filters, reports, admin forms.
- Check typed dates yourself and say what’s wrong with error.
Don’t
- Don’t rely on min and max to stop a typed date; they only limit the calendar.
- Don’t use it for birthdays; three plain fields are easier to correct.
In use
Related
- Date pickerA field that shows the chosen date and opens a month grid in a popover. Values are ISO date strings.
- Memorable dateDay, month and year as three plain fields (the GOV.UK date input) for birthdays and dates people know by heart.
- FieldLabel above, hint under it, error in words between the hint and the control, with the ids and aria wiring done for you.
- ISO datesDates as "2026-10-12" strings: strict and forgiving parsers, range labels with a spoken form, and the date grid’s arrow-key model.
Source
The component’s own files, exactly as they run on this site.
/** * DateField: a date typed in segments (month / day / year in the locale's order and with its * separator), with the calendar button beside it. For analytics filters and anywhere people * type dates fast. For dates people know by heart (birthdays) use MemorableDateInput. * * <DateField label="Start date" value={start} onChange={setStart} /> * * `value` is an ISO date or null; onChange fires with an ISO date once all three segments make * a real date, and with null when one is cleared. The day is kept real when the month changes * under it (Mar 31 → Feb gives Feb 28). * * Keys in a segment: digits type and move on by themselves once the segment can't take more * (a 4 in the month means April), ↑ ↓ step and wrap (starting from today's value), Home / End * jump to the first / last value, ← → and the separator key move between segments, Backspace * erases. Pasting "2026-10-12" (or "12 Oct 2026") fills every segment at once. * Each segment is a spinbutton with a spoken value ("10, October"); a two-digit year becomes * 20xx or 19xx on leaving it. */import { Fragment,