ComponentsForm
Date field
A date typed or stepped one part at a time, in the locale's order, with no calendar.
Import
import { DateField } from "@booleanpress/ui/date-field"Usage
DateField is a group of three segments, day, month and year, in the order and with the separators of the provider's locale (month/day/year in en-US, day.month.year in de-DE); each segment is a spin button. It needs no peer package, so it suits a date of birth or an invoice date where a calendar adds nothing. Name it with aria-labelledby pointing at a visible label, or aria-label.
import { useState } from "react"
import { DateField } from "@booleanpress/ui/date-field"
import { Label } from "@booleanpress/ui/label"
export function InvoiceDate() {
const [date, setDate] = useState<Date | null>(null)
return (
<div className="flex flex-col gap-2">
<Label id="invoice-date">Invoice date</Label>
<DateField aria-labelledby="invoice-date" value={date} onValueChange={setDate} />
</div>
)
}It is uncontrolled with defaultValue, or controlled with value and onValueChange. The value is midnight of the day in the provider's timeZone (the browser's when none is set); onValueChange fires once the three segments are filled, and with null when a filled field loses one. A month or year that leaves the day past the month's end brings it back to the last day. A year of one or two digits becomes the year nearest today, within 50 years, when focus leaves it: 85 is 1985 and 30 is 2030 in 2026.
min and max mark a date outside them invalid, without changing it. size, variant="filled", fluid, disabled and aria-invalid follow the other fields. name submits YYYY-MM-DD in a hidden input. For a calendar as well, use DatePicker.
Examples
Basic
Month, day and year in the locale's order, named by its label.
import { DateField } from "@booleanpress/ui/date-field"
import { Label } from "@booleanpress/ui/label"
export default function DateFieldBasic() {
return (
<div className="flex flex-col gap-2">
<Label id="invoice-date-label">Invoice date</Label>
<DateField aria-labelledby="invoice-date-label" defaultValue={new Date(2026, 9, 14)} />
</div>
)
}Locale order
Inside a BooleanUIProvider with locale="de-DE" and German strings: day, month and year, divided by dots.
import { DateField } from "@booleanpress/ui/date-field"
import { Label } from "@booleanpress/ui/label"
import { BooleanUIProvider } from "@booleanpress/ui/provider"
export default function DateFieldLocaleOrder() {
return (
<BooleanUIProvider locale="de-DE" strings={{ day: "Tag", month: "Monat", year: "Jahr", emptySegment: "Leer" }}>
<div className="flex flex-col gap-2">
<Label id="rechnung-label">Rechnungsdatum</Label>
<DateField aria-labelledby="rechnung-label" defaultValue={new Date(2026, 9, 14)} />
</div>
</BooleanUIProvider>
)
}Min and max
min and max bound the date to October 2026; 3 November is outside, so the field is invalid.
import { DateField } from "@booleanpress/ui/date-field"
import { Label } from "@booleanpress/ui/label"
export default function DateFieldMinMax() {
return (
<div className="flex flex-col gap-2">
<Label id="launch-label">Launch date (October 2026)</Label>
<DateField
aria-labelledby="launch-label"
aria-describedby="launch-hint"
min={new Date(2026, 9, 1)}
max={new Date(2026, 9, 31)}
defaultValue={new Date(2026, 10, 3)}
/>
<p id="launch-hint" className="text-sm text-muted-foreground">
A date outside October marks the field invalid.
</p>
</div>
)
}Sizes
sm is 28 px tall with 12 px text, default 35 px with 14 px, lg 42 px with 16 px.
import { DateField } from "@booleanpress/ui/date-field"
const day = new Date(2026, 9, 14)
export default function DateFieldSizes() {
return (
<div className="flex flex-col items-center gap-3">
<DateField size="sm" aria-label="Small" defaultValue={day} />
<DateField aria-label="Default" defaultValue={day} />
<DateField size="lg" aria-label="Large" defaultValue={day} />
</div>
)
}Filled
variant="filled" draws the grey --field-filled fill.
import { DateField } from "@booleanpress/ui/date-field"
import { Label } from "@booleanpress/ui/label"
export default function DateFieldFilled() {
return (
<div className="flex flex-col gap-2">
<Label id="renewal-label">Renewal date</Label>
<DateField aria-labelledby="renewal-label" variant="filled" defaultValue={new Date(2026, 11, 1)} />
</div>
)
}Disabled
The segments leave the tab order and cannot change.
import { DateField } from "@booleanpress/ui/date-field"
import { Label } from "@booleanpress/ui/label"
export default function DateFieldDisabled() {
return (
<div className="flex flex-col gap-2">
<Label id="joined-label">Customer since</Label>
<DateField aria-labelledby="joined-label" disabled defaultValue={new Date(2026, 2, 9)} />
</div>
)
}Invalid
aria-invalid draws the error edge and sets aria-invalid on every segment; aria-describedby reads the message.
import { DateField } from "@booleanpress/ui/date-field"
import { Label } from "@booleanpress/ui/label"
export default function DateFieldInvalid() {
return (
<div className="flex flex-col gap-2">
<Label id="due-label">Payment due</Label>
<DateField aria-labelledby="due-label" aria-invalid aria-describedby="due-error" />
<p id="due-error" className="text-sm text-destructive-strong">
Enter the day the payment is due.
</p>
</div>
)
}With a form
name submits the date as YYYY-MM-DD; the form's Reset puts defaultValue back, in the segments and the value sent.
import * as React from "react"
import { Button } from "@booleanpress/ui/button"
import { DateField } from "@booleanpress/ui/date-field"
import { Label } from "@booleanpress/ui/label"
export default function DateFieldWithForm() {
const [sent, setSent] = React.useState("")
return (
<form
className="flex w-full max-w-sm flex-col gap-3"
onSubmit={(event) => {
event.preventDefault()
setSent([...new FormData(event.currentTarget)].map(([key, value]) => `${key} = ${value}`).join(", "))
}}
>
<div className="flex flex-col gap-2">
<Label id="send-on-label">Send on</Label>
<DateField aria-labelledby="send-on-label" name="day" defaultValue={new Date(2026, 9, 5)} />
</div>
<div className="flex gap-2">
<Button type="submit">Submit</Button>
<Button type="reset" variant="outline">
Reset
</Button>
</div>
<pre className="overflow-x-auto rounded-md bg-muted px-3 py-2 font-mono text-xs whitespace-pre-wrap text-foreground">{sent ? `Sent: ${sent}` : "Press Submit to see what the form sends."}</pre>
</form>
)
}Controlled with a rule
The parent refuses weekends: each change is only proposed through onValueChange, and the field shows and submits the value it is given.
import * as React from "react"
import { DateField } from "@booleanpress/ui/date-field"
import { Label } from "@booleanpress/ui/label"
export default function DateFieldControlledRule() {
const [value, setValue] = React.useState<Date | null>(new Date(2026, 9, 9))
const [refused, setRefused] = React.useState(false)
return (
<div className="flex flex-col gap-2">
<Label id="delivery-day-label">Delivery day</Label>
<DateField
aria-labelledby="delivery-day-label"
aria-describedby="delivery-day-note"
value={value}
onValueChange={(next) => {
// The parent decides: a weekend is refused, and the field stays on the day it had.
const weekend = next !== null && (next.getDay() === 0 || next.getDay() === 6)
setRefused(weekend)
if (!weekend) setValue(next)
}}
/>
<p id="delivery-day-note" className="text-sm text-muted-foreground">
{refused ? "Weekends are not allowed." : "Monday to Friday only."}
</p>
</div>
)
}Read-only
readOnly shows the date and keeps the segments focusable, but no change is made.
import { DateField } from "@booleanpress/ui/date-field"
import { Label } from "@booleanpress/ui/label"
export default function DateFieldReadOnly() {
return (
<div className="flex flex-col gap-2">
<Label id="signup-label">Customer since</Label>
<DateField aria-labelledby="signup-label" readOnly defaultValue={new Date(2026, 2, 9)} />
</div>
)
}Accessibility
- Semantics
- A
groupholding threespinbuttons, each witharia-valuenow,aria-valuemin,aria-valuemaxandaria-valuetext(the month's name, or "Empty" while unfilled). The separators are hidden from screen readers. The segments are laid out left to right in every direction, in the locale's order. - Labels
- Name the group with
aria-labelledbyoraria-label. Each segment is named by the provider stringsday,monthandyear, after the group's label; an empty one readsemptySegment. The month names come fromIntlin the provider's locale. - Focus
- Each segment is a tab stop; the left and right arrows also move between them. A filled segment moves focus to the next. A press on the field's padding focuses the first empty segment.
- Known limits
- On a phone, the segments are editable text so the number keyboard opens; typing goes through the same rules as a key press.
minandmaxmark the field invalid; they do not stop the arrows or clamp the value.- Dates are on the Gregorian calendar in every locale.
- In Arabic locales the segments read left to right, the day at the left, where Arabic text (and
DatePicker's field) writes a numeric date right to left, the day at the right.
Keyboard
| Key | Behaviour |
|---|---|
| ↑ | Adds one to the segment, wrapping from the highest value to the lowest. |
| ↓ | Takes one from the segment, wrapping from the lowest to the highest. |
| → | Moves to the next segment. |
| ← | Moves to the previous segment. |
| 0–9 | Types into the segment, moving on once it is complete (two digits, or four for the year). |
| Backspace | Removes the segment's last digit; on an empty segment, moves to the previous one. |
| Delete | Empties the segment. |
| Home | Sets the segment to its lowest value. |
| End | Sets the segment to its highest value. |
API
DateField
Renders a div and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | Date | null | The date it starts with, uncontrolled. | |
disabled | boolean | false | Takes the segments out of the tab order and stops every change. |
fluid | boolean | false | Fills the width of its container. |
form | string | The id of the form the value belongs to, for a field placed outside it. | |
max | Date | The latest day; a later date marks the field invalid. | |
min | Date | The earliest day; an earlier date marks the field invalid. | |
name | string | Submits the date as YYYY-MM-DD in a hidden input of this name. | |
onValueChange | ((value: Date | null) => void) | Called with the new date once day, month and year are filled, and with null when a filled field loses one. | |
readOnly | boolean | false | Keeps the segments focusable but stops every change. |
required | boolean | false | Sets aria-required on every segment. |
size | "default" | "sm" | "lg" | 28, 35 or 42 px tall. Defaults to the provider's controlSize. | |
value | Date | null | The date, when you control it: midnight of the day in the provider's time zone. | |
variant | "default" | "filled" | filled fills the field grey. Defaults to the provider's fieldVariant. |
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: .
Provider strings: hour, minute, second, dayPeriod, day, month, year, emptySegment (BooleanUIProvider's strings).
Theming
The field is Input's look (--field, --control, --ring); the focused segment is --primary with --primary-foreground text, like selected text; empty segments are --muted-foreground.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|