Date Input
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/datelibrary.
Features
- Segmented input fields for each date part (month, day, year, hour, minute, second)
- Supports
singleandrangeselection 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
npm install @zag-js/date-input @zag-js/solid # or yarn add @zag-js/date-input @zag-js/solid
npm install @zag-js/date-input @zag-js/vue # or yarn add @zag-js/date-input @zag-js/vue
npm install @zag-js/date-input @zag-js/svelte # or yarn add @zag-js/date-input @zag-js/svelte
Anatomy
Check the date-input anatomy and part names.
Each part includes a
data-partattribute 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
idto theuseMachinehook. 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> ) }
import * as dateInput from "@zag-js/date-input" import { normalizeProps, useMachine } from "@zag-js/solid" import { createUniqueId } from "solid-js" function DateInput() { const service = useMachine(dateInput.machine, { id: createUniqueId() }) 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 {...api().getSegmentProps({ segment })}> {segment.text} </span> ))} </div> </div> <input {...api().getHiddenInputProps()} /> </div> ) }
<script setup> import * as dateInput from "@zag-js/date-input" import { useMachine, normalizeProps } from "@zag-js/vue" import { computed } from "vue" const service = useMachine(dateInput.machine) const api = computed(() => dateInput.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <label v-bind="api.getLabelProps()">Enter a date</label> <div v-bind="api.getControlProps()"> <div v-bind="api.getSegmentGroupProps()"> <span v-for="(segment, i) in api.getSegments()" :key="i" v-bind="api.getSegmentProps({ segment })" > {{ segment.text }} </span> </div> </div> <input v-bind="api.getHiddenInputProps()" /> </div> </template>
<script> import * as dateInput from "@zag-js/date-input" import { useMachine } from "@zag-js/svelte" const service = useMachine(dateInput.machine()) const api = dateInput.connect(service, $service) </script> <div use:api.getRootProps> <label use:api.getLabelProps>Enter a date</label> <div use:api.getControlProps> <div use:api.getSegmentGroupProps> {#each api.getSegments() as segment, i (i)} <span use:api.getSegmentProps={{ segment }}> {segment.text} </span> {/each} </div> </div> <input use: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 usedisabledbooleanWhether the date input is disabled.readOnlybooleanWhether the date input is read-only.requiredbooleanWhether the date input is requiredinvalidbooleanWhether the date input is invalidminDateValueThe 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 focuseddisabledbooleanWhether the date input is disabledinvalidbooleanWhether the date input is invalidgroupCountnumberThe 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.