Skip to main content

A date input lets you enter a date by typing into segmented input fields (month, day, year, etc.) with validation and keyboard navigation.

Good to know: The date input is built on top of the @internationalized/date library.

Loading...

Features

  • Segmented input fields for each date part (month, day, year, hour, minute, second)
  • Supports single and range selection modes
  • Keyboard navigation and auto-advance between segments
  • Placeholder management with visual distinction
  • Customizable granularity (day, month, hour, minute, second)
  • Optional leading zeros in numeric fields
  • Works with localization and timezone
  • Full accessibility with keyboard and screen reader support
  • Form integration with hidden input element

Installation

Install the date-input package:

npm install @zag-js/date-input @zag-js/react # or yarn add @zag-js/date-input @zag-js/react

Anatomy

Check the date-input anatomy and part names.

Each part includes a data-part attribute to help identify them in the DOM.

Usage

Import the package:

import * as dateInput from "@zag-js/date-input"

These are the key exports:

  • machine - State machine logic.
  • connect - Maps machine state to JSX props and event handlers.
  • parse - Parses an ISO 8601 date string.

You'll also need to provide a unique id to the useMachine hook. This is used to ensure that every part has a unique identifier.

Then use the framework integration helpers:

import * as dateInput from "@zag-js/date-input" import { useMachine, normalizeProps } from "@zag-js/react" import { useId } from "react" function DateInput() { const service = useMachine(dateInput.machine, { id: useId() }) const api = dateInput.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Enter a date</label> <div {...api.getControlProps()}> <div {...api.getSegmentGroupProps()}> {api.getSegments().map((segment, i) => ( <span key={i} {...api.getSegmentProps({ segment })}> {segment.text} </span> ))} </div> </div> <input {...api.getHiddenInputProps()} /> </div> ) }

Rendering segments

Use api.getSegments() to get the list of date segments. Each segment has a type (e.g., "month", "day", "year", "literal") and a text property for display.

<div {...api.getControlProps()}> <div {...api.getSegmentGroupProps()}> {api.getSegments().map((segment, i) => ( <span key={i} {...api.getSegmentProps({ segment })}> {segment.text} </span> ))} </div> </div>

For range mode, pass the index to distinguish start and end date segments:

<div {...api.getControlProps()}> <div {...api.getSegmentGroupProps({ index: 0 })}> {api.getSegments({ index: 0 }).map((segment, i) => ( <span key={i} {...api.getSegmentProps({ segment, index: 0 })}> {segment.text} </span> ))} </div> <span></span> <div {...api.getSegmentGroupProps({ index: 1 })}> {api.getSegments({ index: 1 }).map((segment, i) => ( <span key={i} {...api.getSegmentProps({ segment, index: 1 })}> {segment.text} </span> ))} </div> </div>

Setting the initial date

To set the initial value rendered by the date input, set defaultValue.

const service = useMachine(dateInput.machine, { defaultValue: [dateInput.parse("2024-01-15")], })

Controlling the selected date

Use value and onValueChange to programmatically control the selected date.

const service = useMachine(dateInput.machine, { value: [dateInput.parse("2024-01-15")], onValueChange(details) { // details => { value: DateValue[], valueAsString: string[] } console.log("selected date:", details.valueAsString) }, })

You can also set it with api.setValue.

const nextValue = dateInput.parse("2024-01-15") api.setValue([nextValue])

Setting the min and max dates

To constrain the dates that can be entered, set the min and max properties.

const service = useMachine(dateInput.machine, { min: dateInput.parse("2024-01-01"), max: dateInput.parse("2024-12-31"), })

When typing values outside the range, invalid segments will be marked as invalid.

Disabling the date input

Set disabled to true to make the input non-interactive.

const service = useMachine(dateInput.machine, { disabled: true, })

Setting read-only mode

Set readOnly to true to prevent value changes while allowing focus.

const service = useMachine(dateInput.machine, { readOnly: true, })

Required and invalid state

Use required and invalid for form validation and UI state.

const service = useMachine(dateInput.machine, { required: true, invalid: false, })

Controlling date granularity

Set granularity to control the smallest unit displayed in the input.

const service = useMachine(dateInput.machine, { granularity: "day", // "month" | "day" | "year" | "hour" | "minute" | "second" })

The available granularities are: "month", "day", "year", "hour", "minute", and "second".

Forcing leading zeros

Set shouldForceLeadingZeros to always display leading zeros in numeric fields (e.g., "01" instead of "1").

const service = useMachine(dateInput.machine, { shouldForceLeadingZeros: true, })

By default, leading zeros follow the locale's conventions.

Controlling the placeholder date

Use placeholderValue to set the initial placeholder date, which is shown in unfilled segments.

const service = useMachine(dateInput.machine, { placeholderValue: dateInput.parse("2024-01-01"), defaultPlaceholderValue: dateInput.parse("2024-01-01"), })

Listen for placeholder changes with onPlaceholderChange:

const service = useMachine(dateInput.machine, { onPlaceholderChange(details) { // details => { placeholderValue: DateValue, value: DateValue[], valueAsString: string[] } console.log("placeholder changed:", details.placeholderValue) }, })

Listening to focus changes

Use onFocusChange to listen for when the input gains or loses focus.

const service = useMachine(dateInput.machine, { onFocusChange(details) { // details => { focused: boolean } console.log("focused:", details.focused) }, })

Choosing a selection mode

Use selectionMode to allow entering a single date or a date range.

const service = useMachine(dateInput.machine, { selectionMode: "range", // "single" | "range" })

In range mode, the input will have segments for both start and end dates.

Working with display values

The displayValues property tracks partially entered dates while the user is typing. This is useful for showing the editing state before a complete date is entered.

const displayValues = api.displayValues // Each incomplete date shows which segments have been filled in

Clearing the date

Use api.clearValue() to clear the selected date.

api.clearValue()

Accessing segments

Use api.getSegments() to access the individual date segments.

const segments = api.getSegments() segments.forEach((segment) => { console.log(segment.type) // "month", "day", "year", etc. console.log(segment.text) // displayed text console.log(segment.value) // numeric value console.log(segment.isEditable) // whether user can edit })

Then render each segment:

<div {...api.getControlProps()}> <div {...api.getSegmentGroupProps()}> {api.getSegments().map((segment, i) => ( <span key={i} {...api.getSegmentProps({ segment })}> {segment.text} </span> ))} </div> </div>

Checking segment editability

Use api.getSegmentState() to determine if a segment can be edited.

const state = api.getSegmentState({ segment }) console.log(state.editable) // true if the segment is editable

Using the hidden input for forms

The date input includes a hidden input element for form submission.

<input {...api.getHiddenInputProps()} />

Set the name attribute to include it in form data:

const service = useMachine(dateInput.machine, { name: "birthDate", })

Multiple date inputs (range mode) can use a name prop on getHiddenInputProps:

<input {...api.getHiddenInputProps({ index: 0, name: "startDate" })} /> <input {...api.getHiddenInputProps({ index: 1, name: "endDate" })} />

Labeling the input

Use getLabelProps() to properly label the input for accessibility.

<label {...api.getLabelProps()}>Select a date</label> <div {...api.getControlProps()}> {/* segments */} </div>

Setting locale and timezone

Set locale and timeZone to control date parsing and formatting.

const service = useMachine(dateInput.machine, { locale: "en-GB", timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, })

Hour cycle (12-hour vs 24-hour time)

Set hourCycle to control time format display.

const service = useMachine(dateInput.machine, { granularity: "hour", hourCycle: 24, // 12 | 24 })

By default, this is determined by the locale.

Custom formatter

Provide a formatter to customize how dates are parsed and formatted.

import { DateFormatter } from "@internationalized/date" const service = useMachine(dateInput.machine, { formatter: new DateFormatter("en-US", { dateStyle: "short", timeStyle: "short", }), })

Form integration

Use the date input within a form with standard HTML form handling:

<form onSubmit={(e) => { e.preventDefault() const formData = new FormData(e.currentTarget) const date = formData.get("birthDate") console.log("selected date:", date) }} > <label {...api.getLabelProps()}>Birth Date</label> <div {...api.getControlProps()}> <div {...api.getSegmentGroupProps()}> {api.getSegments().map((segment, i) => ( <span key={i} {...api.getSegmentProps({ segment })}> {segment.text} </span> ))} </div> </div> <input {...api.getHiddenInputProps({ name: "birthDate" })} /> <button type="submit">Submit</button> </form>

Listening to date changes

Use onValueChange to listen for date changes.

const service = useMachine(dateInput.machine, { onValueChange(details) { // details => { value: DateValue[], valueAsString: string[] } console.log("selected date:", details.valueAsString) }, })

Localization

Use translations to customize accessibility labels and messages.

const service = useMachine(dateInput.machine, { translations: { placeholder: (locale) => ({ year: "YYYY", month: "MM", day: "DD", hour: "HH", minute: "MM", second: "SS", }), }, })

Styling guide

Each date-input part includes a data-part attribute you can target in CSS.

[data-scope="date-input"][data-part="root"] { /* styles for the root container */ } [data-scope="date-input"][data-part="label"] { /* styles for the label */ } [data-scope="date-input"][data-part="control"] { /* styles for the control container */ } [data-scope="date-input"][data-part="segment-group"] { /* styles for the segment group */ } [data-scope="date-input"][data-part="segment"] { /* styles for each segment */ } [data-scope="date-input"][data-part="hidden-input"] { /* styles for the hidden input */ }

State attributes

[data-scope="date-input"][data-part="root"] { &[data-disabled] { /* styles for disabled state */ } &[data-readonly] { /* styles for read-only state */ } &[data-invalid] { /* styles for invalid state */ } }

Segment states

[data-scope="date-input"][data-part="segment"] { &[data-placeholder-shown] { /* styles for placeholder segments */ } &[data-type="month"] { /* styles for month segments */ } &[data-type="day"] { /* styles for day segments */ } &[data-type="year"] { /* styles for year segments */ } &[data-type="hour"] { /* styles for hour segments */ } &[data-type="minute"] { /* styles for minute segments */ } &[data-type="second"] { /* styles for second segments */ } }

Methods and Properties

Machine Context

The date input machine exposes the following context properties:

  • localestringThe locale (BCP 47 language tag) to use when formatting the date.
  • translationsIntlTranslationsThe localized messages to use.
  • idsPartial<{ root: string; label: (index: number) => string; control: string; segmentGroup: (index: number) => string; hiddenInput: (index: number) => string; }>The ids of the elements in the date input. Useful for composition.
  • namestringThe `name` attribute of the input element.
  • formstringThe `form` attribute of the hidden input element.
  • timeZonestringThe time zone to use
  • disabledbooleanWhether the date input is disabled.
  • readOnlybooleanWhether the date input is read-only.
  • requiredbooleanWhether the date input is required
  • invalidbooleanWhether the date input is invalid
  • minDateValueThe minimum date that can be selected.
  • maxDateValueThe maximum date that can be selected.
  • valueDateValue[]The controlled selected date(s).
  • defaultValueDateValue[]The initial selected date(s) when rendered. Use when you don't need to control the selected date(s).
  • placeholderValueDateValueThe controlled placeholder date.
  • defaultPlaceholderValueDateValueThe initial placeholder date when rendered.
  • onValueChange(details: ValueChangeDetails) => voidFunction called when the value changes.
  • onPlaceholderChange(details: PlaceholderChangeDetails) => voidA function called when the placeholder value changes.
  • onFocusChange(details: FocusChangeDetails) => voidA function called when the date input gains or loses focus.
  • selectionModeSelectionModeThe selection mode of the date input. - `single` - only one date can be entered - `range` - a range of dates can be entered (start and end)
  • hourCycleHourCycleWhether to use 12-hour or 24-hour time format. By default, this is determined by the locale.
  • granularityDateGranularityDetermines the smallest unit that is displayed in the date input.
  • shouldForceLeadingZerosbooleanWhether to always show leading zeros in month, day, and hour fields. When false, formatting follows the locale default (e.g. "1" instead of "01").
  • formatterDateFormatterThe date formatter to use.
  • allSegmentsPartial<{ year: boolean; month: boolean; day: boolean; hour: boolean; minute: boolean; second: boolean; dayPeriod: boolean; era: boolean; literal: boolean; timeZoneName: boolean; weekday: boolean; unknown: boolean; fractionalSecond: boolean; }>The computed segments map for the formatter.
  • format(date: FormatDateDetails) => stringThe format function for converting a DateValue to a string.
  • dir"ltr" | "rtl"The document's text/writing direction.
  • idstringThe unique identifier of the machine.
  • getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.

Machine API

The date input api exposes the following methods:

  • focusedbooleanWhether the date input is focused
  • disabledbooleanWhether the date input is disabled
  • invalidbooleanWhether the date input is invalid
  • groupCountnumberThe number of segment groups rendered by the date input.
  • valueDateValue[]The selected date(s).
  • valueAsDateDate[]The selected date(s) as Date objects.
  • valueAsStringstring[]The selected date(s) as strings.
  • placeholderValueDateValueThe placeholder date.
  • displayValuesIncompleteDate[]Per-group editing state. Each IncompleteDate tracks which segments have been filled in by the user (non-null = entered, null = placeholder).
  • setValue(values: DateValue[]) => voidSets the selected date(s) to the given values.
  • clearValueVoidFunctionClears the selected date(s).
  • getSegments(props?: SegmentsProps) => DateSegment[]Returns the segments for the given index.
  • getSegmentState(props: SegmentProps) => SegmentStateReturns the state details for a given segment.

Data Attributes

Root
data-scope
date-input
data-part
root
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
Label
data-scope
date-input
data-part
label
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
Control
data-scope
date-input
data-part
control
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
data-focus
Present when focused
SegmentGroup
data-scope
date-input
data-part
segment-group
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
data-focus
Present when focused
Segment
data-scope
date-input
data-part
segment
data-type
The type of the item
data-readonly
Present when read-only
data-disabled
Present when disabled
data-value
The value of the item

CSS Variables

Edit this page on GitHub