A date picker lets users enter a date through a text input or pick one from a calendar popup. It is built on top of the @internationalized/date library and supports localization, timezones, and custom calendar systems.
Features
- Single, multiple, and range selection modes
- Disabling specific dates
- Date range presets
- Week numbers
- Custom format and parse logic
- Localization, timezone, and custom calendar system support
- Keyboard-accessible calendar navigation
Installation
npm install @zag-js/date-picker @zag-js/react
Import the date picker package:
import * as datepicker from "@zag-js/date-picker"
import { useMachine, normalizeProps } from "@zag-js/react"
import { useId } from "react"
The date picker package exports three key functions:
machine — Behavior logic.
connect — Maps behavior to JSX props and event handlers.
parse — Parses an ISO 8601 date string into a DateValue.
import * as datepicker from "@zag-js/date-picker"
import { useMachine, normalizeProps } from "@zag-js/react"
import { useId } from "react"
export function DatePicker() {
const service = useMachine(datepicker.machine, {
id: useId(),
locale: "en-US",
})
const api = datepicker.connect(service, normalizeProps)
return (
<div {...api.getRootProps()}>
<label {...api.getLabelProps()}>Date</label>
<div {...api.getControlProps()}>
<input {...api.getInputProps()} />
<button {...api.getTriggerProps()}>📅</button>
<button {...api.getClearTriggerProps()}>✕</button>
</div>
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>
<div {...api.getViewControlProps({ view: "day" })}>
<button {...api.getPrevTriggerProps()}>‹</button>
<button {...api.getViewTriggerProps()}>
{api.visibleRangeText.start}
</button>
<button {...api.getNextTriggerProps()}>›</button>
</div>
<table {...api.getTableProps({ view: "day" })}>
<thead {...api.getTableHeadProps()}>
<tr {...api.getTableRowProps()}>
{api.weekDays.map((day, i) => (
<th key={i} aria-label={day.long}>
{day.narrow}
</th>
))}
</tr>
</thead>
<tbody {...api.getTableBodyProps()}>
{api.weeks.map((week, weekIndex) => (
<tr key={weekIndex} {...api.getTableRowProps()}>
{week.map((value, i) => (
<td key={i} {...api.getDayTableCellProps({ value })}>
<div {...api.getDayTableCellTriggerProps({ value })}>
{value.day}
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}
Setting the initial date
Set defaultValue to define the initial selected date.
const service = useMachine(datepicker.machine, {
defaultValue: [datepicker.parse("2024-01-15")],
})
Controlled date
Use value and onValueChange to control the selected date programmatically.
const [value, setValue] = useState([datepicker.parse("2024-01-15")])
const service = useMachine(datepicker.machine, {
value,
onValueChange(details) {
// details => { value: DateValue[], valueAsString: string[], view: "day" | "month" | "year" }
setValue(details.value)
},
})
Setting min and max dates
Use min and max to constrain the selectable date range. Navigation triggers are disabled when the boundary is reached.
const service = useMachine(datepicker.machine, {
min: datepicker.parse("2024-01-01"),
max: datepicker.parse("2024-12-31"),
})
Selection modes
Use selectionMode to switch between "single", "multiple", and "range".
const service = useMachine(datepicker.machine, {
selectionMode: "range",
})
For "multiple" mode, cap selections with maxSelectedDates:
const service = useMachine(datepicker.machine, {
selectionMode: "multiple",
maxSelectedDates: 3,
})
Marking unavailable dates
Use isDateUnavailable to block specific dates.
import { isWeekend } from "@internationalized/date"
const service = useMachine(datepicker.machine, {
isDateUnavailable: (date, locale) => isWeekend(date, locale),
})
Using range presets
In "range" mode, use api.getPresetTriggerProps with a preset key or a custom DateValue[].
<button {...api.getPresetTriggerProps({ value: "last7Days" })}>Last 7 days</button>
<button {...api.getPresetTriggerProps({ value: "last30Days" })}>Last 30 days</button>
<button
{...api.getPresetTriggerProps({
value: [datepicker.parse("2024-01-01"), datepicker.parse("2024-01-15")],
})}
>
First half of January
</button>
Controlling the calendar view
Set defaultView to start in "day", "month", or "year" view.
const service = useMachine(datepicker.machine, {
defaultView: "month",
})
Restrict navigable views with minView and maxView:
const service = useMachine(datepicker.machine, {
minView: "month",
maxView: "year",
})
Rendering inline
Set inline to true to render the calendar without a popup.
const service = useMachine(datepicker.machine, {
inline: true,
})
Fixed weeks
Set fixedWeeks to always render 6 weeks and avoid layout jumps between months.
const service = useMachine(datepicker.machine, {
fixedWeeks: true,
})
Week numbers
Set showWeekNumbers to true to render a week-number column.
const service = useMachine(datepicker.machine, {
showWeekNumbers: true,
})
Multiple months
Set numOfMonths and use api.getOffset to render adjacent months.
const service = useMachine(datepicker.machine, {
numOfMonths: 2,
})
const offset = api.getOffset({ months: 1 })
Locale and timezone
const service = useMachine(datepicker.machine, {
locale: "en-GB",
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
})
Custom calendar systems
Pass a createCalendar function to support non-Gregorian calendars.
import { PersianCalendar } from "@internationalized/date"
const service = useMachine(datepicker.machine, {
locale: "fa-IR",
createCalendar: (identifier) => {
if (identifier === "persian") return new PersianCalendar()
throw new Error(`Unsupported calendar: ${identifier}`)
},
})
Custom format and parse
Use format and parse to control how dates are displayed and parsed in the input.
const service = useMachine(datepicker.machine, {
format: (date, details) =>
date.toDate(details.timeZone).toLocaleDateString(details.locale, { dateStyle: "short" }),
parse: (value) => datepicker.parse(value),
})
Usage within forms
Set name to include the date value in form data.
const service = useMachine(datepicker.machine, {
name: "appointment-date",
})
API Reference
The controlled selected date(s).
The initial selected date(s) when rendered.
selectionMode
"single" | "multiple" | "range"
default:"\"single\""
The selection mode of the calendar.
The minimum selectable date.
The maximum selectable date.
Whether the date picker is disabled.
Whether the date picker is read-only.
Whether the date picker is required.
Whether the date picker is in an invalid state.
Whether to render the calendar inline (without a popup).
defaultView
"day" | "month" | "year"
default:"\"day\""
The initial calendar view.
minView
"day" | "month" | "year"
default:"\"day\""
The minimum navigable calendar view.
maxView
"day" | "month" | "year"
default:"\"year\""
The maximum navigable calendar view.
The number of months to display simultaneously.
The first day of the week. 0 = Sunday, 6 = Saturday.
Whether to always render 6 weeks per month.
Whether to show a week-number column in day view.
locale
string
default:"\"en-US\""
The BCP 47 locale tag used for formatting.
The IANA time zone identifier.
isDateUnavailable
(date: DateValue, locale: string) => boolean
Function to mark specific dates as unavailable.
format
(date: DateValue, details: LocaleDetails) => string
Custom date-to-string formatter for the input.
parse
(value: string, details: LocaleDetails) => DateValue | undefined
Custom string-to-date parser for the input.
Popup positioning options.
onValueChange
(details: { value: DateValue[], valueAsString: string[], view: DateView }) => void
Callback invoked when the selected date changes.
onViewChange
(details: { view: DateView }) => void
Callback invoked when the calendar view changes.
onOpenChange
(details: { open: boolean, value: DateValue[] }) => void
Callback invoked when the popup opens or closes.
Styling
Each date picker part includes a data-part attribute and data-scope="date-picker" you can target in CSS.
| Part | Description |
|---|
root | The root container |
label | The label element |
control | Wraps input and trigger |
input | The text input |
trigger | The calendar open button |
clear-trigger | The clear button |
positioner | Positions the popup |
content | The popup content |
view-control | Navigation controls row |
view-trigger | Button showing the visible range |
prev-trigger | Previous month/year button |
next-trigger | Next month/year button |
table | The calendar grid |
table-cell | A date cell |
table-cell-trigger | A date cell button |
Open and closed state
[data-scope="date-picker"][data-part="trigger"][data-state="open"] {
/* styles for open state */
}
[data-scope="date-picker"][data-part="trigger"][data-state="closed"] {
/* styles for closed state */
}
Cell states
[data-scope="date-picker"][data-part="table-cell-trigger"] {
/* base cell styles */
}
[data-scope="date-picker"][data-part="table-cell-trigger"][data-selected] {
/* selected date */
}
[data-scope="date-picker"][data-part="table-cell-trigger"][data-focus] {
/* focused date */
}
[data-scope="date-picker"][data-part="table-cell-trigger"][data-disabled] {
/* disabled date */
}
[data-scope="date-picker"][data-part="table-cell-trigger"][data-unavailable] {
/* unavailable date */
}
[data-scope="date-picker"][data-part="table-cell-trigger"][data-today] {
/* today's date */
}
[data-scope="date-picker"][data-part="table-cell-trigger"][data-weekend] {
/* weekend date */
}
Range selection states
[data-scope="date-picker"][data-part="table-cell-trigger"][data-range-start] { }
[data-scope="date-picker"][data-part="table-cell-trigger"][data-range-end] { }
[data-scope="date-picker"][data-part="table-cell-trigger"][data-in-range] { }
[data-scope="date-picker"][data-part="table-cell-trigger"][data-in-hover-range] { }
[data-scope="date-picker"][data-part="table-cell-trigger"][data-hover-range-start] { }
[data-scope="date-picker"][data-part="table-cell-trigger"][data-hover-range-end] { }
Accessibility
The date picker renders an accessible calendar grid with proper ARIA roles. Each date cell has an aria-label with the full date text, and the visible range is announced to screen readers when navigation occurs.
Keyboard interactions
| Key | Description |
|---|
Enter / Space | Opens the calendar or selects the focused date. |
Escape | Closes the calendar popup. |
ArrowLeft | Moves focus to the previous day. |
ArrowRight | Moves focus to the next day. |
ArrowUp | Moves focus to the same day in the previous week. |
ArrowDown | Moves focus to the same day in the next week. |
PageUp | Moves focus to the same day in the previous month. |
PageDown | Moves focus to the same day in the next month. |
Shift + PageUp | Moves focus to the same day in the previous year. |
Shift + PageDown | Moves focus to the same day in the next year. |
Home | Moves focus to the first day of the current week. |
End | Moves focus to the last day of the current week. |