Skip to the content

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 group holding three spinbuttons, each with aria-valuenow, aria-valuemin, aria-valuemax and aria-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-labelledby or aria-label. Each segment is named by the provider strings day, month and year, after the group's label; an empty one reads emptySegment. The month names come from Intl in 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.
  • min and max mark 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

Keyboard
KeyBehaviour
↑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–9Types into the segment, moving on once it is complete (two digits, or four for the year).
BackspaceRemoves the segment's last digit; on an empty segment, moves to the previous one.
DeleteEmpties the segment.
HomeSets the segment to its lowest value.
EndSets the segment to its highest value.

API

DateField

Renders a div and passes it every other prop.

DateField props
PropTypeDefaultDescription
defaultValueDate | nullThe date it starts with, uncontrolled.
disabledbooleanfalseTakes the segments out of the tab order and stops every change.
fluidbooleanfalseFills the width of its container.
formstringThe id of the form the value belongs to, for a field placed outside it.
maxDateThe latest day; a later date marks the field invalid.
minDateThe earliest day; an earlier date marks the field invalid.
namestringSubmits 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.
readOnlybooleanfalseKeeps the segments focusable but stops every change.
requiredbooleanfalseSets aria-required on every segment.
size"default" | "sm" | "lg"28, 35 or 42 px tall. Defaults to the provider's controlSize.
valueDate | nullThe 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.

Theme tokens
TokenUsed for