Segmented date and time inputs that users type into or step with the arrow keys. DateField and TimeField format every segment for the user's locale, support time zones and any granularity down to seconds, and validate against min/max, required and custom rules.
DateField takes a CalendarDate, CalendarDateTime or ZonedDateTime, and
TimeField takes a Time (or either date-time type). Create them with
parseDate, parseDateTime, parseZonedDateTime, today() or new Time()
from @internationalized/date.
onChange gives you the same type back, so time zones and calendar systems
are never lost in a round trip.
<DateField> {/* or <TimeField> */} <Label /> {/* from label */} <DateInput> {/* the field chrome */} <DateSegment /> {/* month */} <DateSegment /> {/* literal "/" */} <DateSegment /> {/* day, year, hour, … */} </DateInput> <Description /> {/* from description */} <FieldError /> {/* from errorMessage / validation */} <input type="hidden" /> {/* for form submission */}</DateField>
Part
Renders
Notes
DateField
<div>
Root. Labels, validation and the hidden form input. data-slot="date-field".
TimeField
<div>
Same structure for times. data-slot="time-field".
DateInput
<div role="group">
Draws the field chrome with the shared variant and size styles. data-slot="date-input".
Segment
<span role="spinbutton">
One per editable part. Literals (/, :, spaces) are plain spans with data-type="literal".
Label
<span>
Rendered when label is set. Adds a red * when isRequired.
Description
<span slot="description">
Rendered when description is set.
FieldError
<span>
Shown when the field is invalid. Uses errorMessage, or the browser's message.
DateInput is exported on its own, so you can build a custom layout inside React Aria's DateField or TimeField and still get the library's field styles.
granularity sets the smallest segment shown: day (default for dates), hour, minute or second. Time granularities need a value that has a time, such as CalendarDateTime or ZonedDateTime; without a value, the field picks the right type for you.
With a ZonedDateTime value the field shows the time zone abbreviation and handles daylight saving transitions correctly. Convert between zones with toTimeZone. hideTimeZone hides the abbreviation when the zone is shown elsewhere.
Launch (New York)
3/15/2027, 9:00AMEDT
Same moment in Tokyo
3/15/2027, 10:00PMGMT+9
Time only, zone hidden
9:00AM
import { parseZonedDateTime, toTimeZone, type ZonedDateTime,} from "@internationalized/date";import { useState } from "react";import { DateField, TimeField } from "@/components/ui/date-field";export default function DateFieldTimeZones() { const [launch, setLaunch] = useState<ZonedDateTime | null>( parseZonedDateTime("2027-03-15T09:00[America/New_York]"), ); return ( <div className="flex w-full max-w-72 flex-col gap-5"> <DateField label="Launch (New York)" value={launch} onChange={setLaunch} granularity="minute" /> <DateField label="Same moment in Tokyo" value={launch ? toTimeZone(launch, "Asia/Tokyo") : null} granularity="minute" isReadOnly /> <TimeField label="Time only, zone hidden" value={launch} onChange={setLaunch} hideTimeZone /> </div> );}
When the field is empty, arrow keys start from today (or midnight). placeholderValue changes that starting point, which saves a lot of key presses for birthdays, and also sets the value type when there's no value.
Date of birth
mm/dd/yyyy
Arrow keys start from 1990.
Next maintenance window
mm/dd/yyyy, ––:––AM
Arrow keys start from 2:00 AM.
import { CalendarDate, CalendarDateTime } from "@internationalized/date";import { DateField } from "@/components/ui/date-field";export default function DateFieldPlaceholderValue() { return ( <div className="flex w-full max-w-60 flex-col gap-5"> <DateField label="Date of birth" placeholderValue={new CalendarDate(1990, 1, 1)} description="Arrow keys start from 1990." /> <DateField label="Next maintenance window" granularity="minute" placeholderValue={new CalendarDateTime(2027, 1, 1, 2, 0)} description="Arrow keys start from 2:00 AM." /> </div> );}
minValue and maxValue mark out-of-range values as invalid and show a localized error. With the default validationBehavior="native" the error appears after the user changes the value or submits the form; this example uses "aria" so it shows immediately.
Report period end
10/10/2026
Within the last 12 months.Value must be 10/5/2026 or earlier.
import { getLocalTimeZone, today } from "@internationalized/date";import { DateField } from "@/components/ui/date-field";export default function DateFieldMinMax() { const now = today(getLocalTimeZone()); return ( <DateField label="Report period end" validationBehavior="aria" minValue={now.subtract({ years: 1 })} maxValue={now} defaultValue={now.add({ days: 5 })} description="Within the last 12 months." className="w-full max-w-60" /> );}
isRequired, minValue / maxValue and a custom validate function run together. Pass a function to errorMessage to tailor the text per failure using validationDetails. With validationBehavior="aria" on the form, errors appear as the user types instead of on submit.
import { type DateValue, getLocalTimeZone, isWeekend, today,} from "@internationalized/date";import { Form, useLocale } from "react-aria-components";import { Button } from "@/components/ui/button";import { DateField } from "@/components/ui/date-field";export default function DateFieldValidation() { const { locale } = useLocale(); return ( <Form validationBehavior="aria" className="flex w-full max-w-60 flex-col gap-4" onSubmit={(e) => e.preventDefault()} > <DateField label="Go-live date" isRequired minValue={today(getLocalTimeZone())} validate={(date: DateValue | null) => date && isWeekend(date, locale) ? "Releases ship on weekdays only." : null } errorMessage={(v) => v.validationDetails.valueMissing ? "Choose a go-live date." : v.validationDetails.rangeUnderflow ? "The date can't be in the past." : v.validationErrors.join(" ") } /> <Button type="submit" className="self-start"> Schedule </Button> </Form> );}
Segment order, separators, the day period and digits follow the locale. Wrap fields in I18nProvider to set one explicitly, and add a -u-ca- extension (fa-IR-u-ca-persian) for other calendar systems.
placeholderValue starts the year segment in 1990, autoComplete="bday" enables autofill, and validate enforces a minimum age. The computed age is shown in the description.
import { CalendarDate, type DateValue, getLocalTimeZone, today,} from "@internationalized/date";import { useState } from "react";import { Form } from "react-aria-components";import { Button } from "@/components/ui/button";import { DateField } from "@/components/ui/date-field";function ageOn(birth: DateValue, on: DateValue) { const years = on.year - birth.year; const hadBirthday = on.month > birth.month || (on.month === birth.month && on.day >= birth.day); return hadBirthday ? years : years - 1;}export default function DateFieldRecipeDateOfBirth() { const now = today(getLocalTimeZone()); const [birth, setBirth] = useState<CalendarDate | null>(null); const age = birth ? ageOn(birth, now) : null; return ( <Form validationBehavior="aria" className="flex w-full max-w-72 flex-col gap-4 rounded-xl border bg-card p-5 shadow-xs" onSubmit={(e) => e.preventDefault()} > <div> <h3 className="font-semibold text-sm">Verify your age</h3> <p className="text-muted-foreground text-xs"> You must be 18 or older to open an account. </p> </div> <DateField label="Date of birth" name="dob" value={birth} onChange={setBirth} isRequired autoComplete="bday" placeholderValue={new CalendarDate(1990, 1, 1)} maxValue={now} validate={(date) => date && ageOn(date, now) < 18 ? "You must be at least 18 years old." : null } description={age !== null && age >= 0 ? `Age: ${age}` : undefined} /> <Button type="submit">Continue</Button> </Form> );}
One row per day with a Switch and a pair of small TimeFields. Each time field is labelled with aria-label, and the closing time is flagged with isInvalid when it isn't after the opening time.
9:00AM
to
5:00PM
9:00AM
to
5:00PM
9:00AM
to
5:00PM
9:00AM
to
8:00PM
9:00AM
to
3:00PM
Closed
Closed
import { Time } from "@internationalized/date";import { useState } from "react";import { TimeField } from "@/components/ui/date-field";import { Switch } from "@/components/ui/switch";type Day = { id: string; name: string; open: boolean; from: Time | null; to: Time | null;};const initial: Day[] = [ { id: "mon", name: "Monday", open: true, from: new Time(9), to: new Time(17), }, { id: "tue", name: "Tuesday", open: true, from: new Time(9), to: new Time(17), }, { id: "wed", name: "Wednesday", open: true, from: new Time(9), to: new Time(17), }, { id: "thu", name: "Thursday", open: true, from: new Time(9), to: new Time(20), }, { id: "fri", name: "Friday", open: true, from: new Time(9), to: new Time(15), }, { id: "sat", name: "Saturday", open: false, from: new Time(10), to: new Time(14), }, { id: "sun", name: "Sunday", open: false, from: null, to: null },];export default function DateFieldRecipeBusinessHours() { const [days, setDays] = useState(initial); const update = (id: string, patch: Partial<Day>) => setDays((prev) => prev.map((d) => (d.id === id ? { ...d, ...patch } : d))); return ( <div className="w-full max-w-lg divide-y rounded-xl border bg-card shadow-xs"> {days.map((day) => { const invalid = day.open && day.from && day.to ? day.to.compare(day.from) <= 0 : false; return ( <div key={day.id} className="flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3" > <Switch size="sm" isSelected={day.open} onChange={(open) => update(day.id, { open })} className="w-32" > {day.name} </Switch> {day.open ? ( <div className="flex flex-1 items-start gap-2"> <TimeField aria-label={`${day.name} opens`} size="sm" value={day.from} onChange={(from) => update(day.id, { from })} className="w-28" /> <span className="pt-1 text-muted-foreground text-xs">to</span> <TimeField aria-label={`${day.name} closes`} size="sm" value={day.to} onChange={(to) => update(day.id, { to })} isInvalid={invalid} errorMessage="Must be after opening." className="w-28" /> </div> ) : ( <span className="text-muted-foreground text-sm">Closed</span> )} </div> ); })} </div> );}
A date, start and end TimeFields and a time zone Select. toCalendarDateTime and toZoned combine them into one instant, shown back in the viewer's own time zone.
The input is a role="group" labelled by label (or aria-label), and each segment is a role="spinbutton" announced with its name ("month", "hour") and current value.
Segments are ordered and labelled for the locale, including right-to-left layouts.
Typing digits fills a segment and moves to the next one automatically. Typing a letter in the day period segment switches AM/PM.
description and the error message are linked with aria-describedby. Invalid fields set aria-invalid.
When name is set, a hidden input carries the value for native form submission and validation.
Use autoComplete (for example bday) so browsers can autofill.
dateSegmentStyles is the class string applied to every segment. DatePicker reuses it, and you can too when composing your own inputs:
tsx
import { dateSegmentStyles } from "@/components/ui/date-field";<DateSegment segment={segment} className={dateSegmentStyles} />
The chrome comes from the shared fieldVariants in field.tsx, so variant and size match Text Field and Select. className on the root accepts a function of the render props (isInvalid, isDisabled, isReadOnly, isRequired, state).