Skip to main content
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

Usage

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

value
DateValue[]
The controlled selected date(s).
defaultValue
DateValue[]
The initial selected date(s) when rendered.
selectionMode
"single" | "multiple" | "range"
default:"\"single\""
The selection mode of the calendar.
min
DateValue
The minimum selectable date.
max
DateValue
The maximum selectable date.
disabled
boolean
Whether the date picker is disabled.
readOnly
boolean
Whether the date picker is read-only.
required
boolean
Whether the date picker is required.
invalid
boolean
Whether the date picker is in an invalid state.
inline
boolean
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.
numOfMonths
number
The number of months to display simultaneously.
startOfWeek
number
The first day of the week. 0 = Sunday, 6 = Saturday.
fixedWeeks
boolean
Whether to always render 6 weeks per month.
showWeekNumbers
boolean
Whether to show a week-number column in day view.
locale
string
default:"\"en-US\""
The BCP 47 locale tag used for formatting.
timeZone
string
default:"\"UTC\""
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.
positioning
PositioningOptions
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.

Parts

PartDescription
rootThe root container
labelThe label element
controlWraps input and trigger
inputThe text input
triggerThe calendar open button
clear-triggerThe clear button
positionerPositions the popup
contentThe popup content
view-controlNavigation controls row
view-triggerButton showing the visible range
prev-triggerPrevious month/year button
next-triggerNext month/year button
tableThe calendar grid
table-cellA date cell
table-cell-triggerA 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

KeyDescription
Enter / SpaceOpens the calendar or selects the focused date.
EscapeCloses the calendar popup.
ArrowLeftMoves focus to the previous day.
ArrowRightMoves focus to the next day.
ArrowUpMoves focus to the same day in the previous week.
ArrowDownMoves focus to the same day in the next week.
PageUpMoves focus to the same day in the previous month.
PageDownMoves focus to the same day in the next month.
Shift + PageUpMoves focus to the same day in the previous year.
Shift + PageDownMoves focus to the same day in the next year.
HomeMoves focus to the first day of the current week.
EndMoves focus to the last day of the current week.

Build docs developers (and LLMs) love