Date Picker
A datepicker allows users to enter a date either through text input, or by choosing a date from the calendar.
Good to know: The date picker machine is built around the ISO 8601 date format
S | M | T | W | T | F | S |
---|---|---|---|---|---|---|
28 | 29 | 30 | 31 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
Jan | Feb | Mar | Apr | ||||||||||||
May | Jun | Jul | Aug | ||||||||||||
Sep | Oct | Nov | Dec |
2019 | 2020 | 2021 | 2022 | ||||||||||||
2023 | 2024 | 2025 | 2026 | ||||||||||||
2027 | 2028 | 2029 | 2030 |
Features
- Displays a calendar view for date selection
- Support for date range selection
- Support for disabling specific dates
- Localization support
- Provides keyboard accessibility for navigating the calendar.
Installation
To use the date-picker machine in your project, run the following command in your command line:
npm install @zag-js/date-picker @zag-js/react # or yarn add @zag-js/date-picker @zag-js/react
npm install @zag-js/date-picker @zag-js/solid # or yarn add @zag-js/date-picker @zag-js/solid
npm install @zag-js/date-picker @zag-js/vue # or yarn add @zag-js/date-picker @zag-js/vue
This command will install the framework agnostic date-picker logic and the reactive utilities for your framework of choice.
Anatomy
To set up the date-picker correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the date picker package into your project
import * as datepicker from "@zag-js/date-picker"
The date picker package exports these key functions:
machine
— The state machine logic for the date-picker widget.connect
— The function that translates the machine's state to JSX attributes and event handlers.parse
- The function that parses the date string into a date object. This function uses@internationalized/date
under the hood.
You'll also need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the date-picker machine in your project 🔥
import * as datepicker from "@zag-js/date-picker" import { useMachine, normalizeProps, Portal } from "@zag-js/react" import { useId } from "react" function DatePicker() { const [state, send] = useMachine(datepicker.machine({ id: useId() })) const api = datepicker.connect(state, send, normalizeProps) return ( <> <div {...api.getControlProps()}> <input {...api.getInputProps()} /> <button {...api.getTriggerProps()}>🗓</button> </div> <Portal> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> {/* Day View */} <div hidden={api.view !== "day"}> <div {...api.getViewControlProps({ view: "year" })}> <button {...api.getPrevTriggerProps()}>Prev</button> <button {...api.getViewTriggerProps()}> {api.visibleRangeText.start} </button> <button {...api.getNextTriggerProps()}>Next</button> </div> <table {...api.getTableProps({ view: "day" })}> <thead {...api.getTableHeaderProps({ view: "day" })}> <tr {...api.getTableRowProps({ view: "day" })}> {api.weekDays.map((day, i) => ( <th scope="col" key={i} aria-label={day.long}> {day.narrow} </th> ))} </tr> </thead> <tbody {...api.getTableBodyProps({ view: "day" })}> {api.weeks.map((week, i) => ( <tr key={i} {...api.getTableRowProps({ view: "day" })}> {week.map((value, i) => ( <td key={i} {...api.getDayTableCellProps({ value })}> <div {...api.getDayTableCellTriggerProps({ value })}> {value.day} </div> </td> ))} </tr> ))} </tbody> </table> </div> {/* Month View */} <div hidden={api.view !== "month"}> <div {...api.getViewControlProps({ view: "month" })}> <button {...api.getPrevTriggerProps({ view: "month" })}> Prev </button> <button {...api.getViewTriggerProps({ view: "month" })}> {api.visibleRange.start.year} </button> <button {...api.getNextTriggerProps({ view: "month" })}> Next </button> </div> <table {...api.getTableProps({ view: "month", columns: 4 })}> <tbody {...api.getTableBodyProps({ view: "month" })}> {api .getMonthsGrid({ columns: 4, format: "short" }) .map((months, row) => ( <tr key={row} {...api.getTableRowProps()}> {months.map((month, index) => ( <td key={index} {...api.getMonthTableCellProps({ ...month, columns: 4, })} > <div {...api.getMonthTableCellTriggerProps({ ...month, columns: 4, })} > {month.label} </div> </td> ))} </tr> ))} </tbody> </table> </div> {/* Year View */} <div hidden={api.view !== "year"}> <div {...api.getViewControlProps({ view: "year" })}> <button {...api.getPrevTriggerProps({ view: "year" })}> Prev </button> <span> {api.getDecade().start} - {api.getDecade().end} </span> <button {...api.getNextTriggerProps({ view: "year" })}> Next </button> </div> <table {...api.getTableProps({ view: "year", columns: 4 })}> <tbody {...api.getTableBodyProps()}> {api.getYearsGrid({ columns: 4 }).map((years, row) => ( <tr key={row} {...api.getTableRowProps({ view: "year" })}> {years.map((year, index) => ( <td key={index} {...api.getYearTableCellProps({ ...year, columns: 4, })} > <div {...api.getYearTableCellTriggerProps({ ...year, columns: 4, })} > {year.label} </div> </td> ))} </tr> ))} </tbody> </table> </div> </div> </div> </Portal> </> ) }
import * as datepicker from "@zag-js/date-picker" import { useMachine, normalizeProps } from "@zag-js/solid" import { createMemo, createUniqueId, Portal } from "solid-js" function DatePicker() { const [state, send] = useMachine(datepicker.machine({ id: createUniqueId() })) const api = createMemo(() => datepicker.connect(state, send, normalizeProps)) return ( <> <div {...api().getControlProps()}> <input {...api().getInputProps()} /> <button {...api().getTriggerProps()}>🗓</button> </div> <Portal> <div {...api().getPositionerProps()}> <div {...api().getContentProps()}> {/* Day View */} <div hidden={api().view !== "day"}> <div {...api().getViewControlProps()}> <button {...api().getPrevTriggerProps()}>Prev</button> <button {...api().getViewTriggerProps()}> {api().visibleRangeText.start} </button> <button {...api().getNextTriggerProps()}>Next</button> </div> <table {...api().getTableProps()}> <thead {...api().getTableHeaderProps()}> <tr {...api().getTableBodyProps()}> <Index each={api().weekDays}> {(day) => ( <th scope="col" aria-label={day().long}> {day().narrow} </th> )} </Index> </tr> </thead> <tbody {...api().getTableBodyProps()}> <Index each={api().weeks}> {(week) => ( <tr {...api().getTableRowProps()}> <Index each={week()}> {(value) => ( <td {...api().getDayTableCellProps({ value: value(), })} > <div {...api().getDayTableCellTriggerProps({ value: value(), })} > {value().day} </div> </td> )} </Index> </tr> )} </Index> </tbody> </table> </div> {/* Month View */} <div hidden={api().view !== "month"}> <div {...api().getViewControlProps({ view: "month" })}> <button {...api().getPrevTriggerProps({ view: "month" })}> Prev </button> <button {...api().getViewTriggerProps({ view: "month" })}> {api().visibleRange.start.year} </button> <button {...api().getNextTriggerProps({ view: "month" })}> Next </button> </div> <table {...api().getTableProps({ view: "month", columns: 4 })}> <tbody> <Index each={api().getMonthsGrid({ columns: 4, format: "short" })} > {(months) => ( <tr {...api().getTableBodyProps({ view: "month" })}> <Index each={months()}> {(month) => ( <td {...api().getMonthTableCellProps(month())}> <div {...api().getMonthTableCellTriggerProps( month(), )} > {month().label} </div> </td> )} </Index> </tr> )} </Index> </tbody> </table> </div> {/* Year View */} <div hidden={api().view !== "year"}> <div {...api().getViewControlProps({ view: "year" })}> <button {...api().getPrevTriggerProps({ view: "year" })}> Prev </button> <span> {api().getDecade().start} - {api().getDecade().end} </span> <button {...api().getNextTriggerProps({ view: "year" })}> Next </button> </div> <table {...api().getTableProps({ view: "year", columns: 4 })}> <tbody> <Index each={api().getYearsGrid({ columns: 4 })}> {(years) => ( <tr {...api().getTableBodyProps({ view: "year" })}> <Index each={years()}> {(year) => ( <td {...api().getYearTableCellProps({ ...year(), columns: 4, })} > <div {...api().getYearTableCellTriggerProps({ ...year(), columns: 4, })} > {year().label} </div> </td> )} </Index> </tr> )} </Index> </tbody> </table> </div> </div> </div> </Portal> </> ) }
<script setup lang="ts"> import * as datepicker from "@zag-js/date-picker" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, Fragment, Teleport } from "vue" const [state, send] = useMachine( datepicker.machine({ id: "1", }) ) const api = computed(() => datepicker.connect(state.value, send, normalizeProps), ) </script> <template> <> <div v-bind="api.getControlProps()"> <input v-bind="api.getInputProps()" /> <button v-bind="api.getTriggerProps()">🗓</button> </div> <Teleport to="body"> <div v-bind="api.getPositionerProps()"> <div v-bind="api.getContentProps()"> <!-- Day View --> <div v-show="api.view !== 'day'"> <div v-bind="api.getViewControlProps({ view: 'year' })"> <button v-bind="api.getPrevTriggerProps()">Prev</button> <button v-bind="api.getViewTriggerProps()"> {{api.visibleRangeText.start}} </button> <button v-bind="api.getNextTriggerProps()">Next</button> </div> <table v-bind="api.getTableProps({ view: 'day' })"> <thead v-bind="api.getTableHeaderProps({ view: 'day' })"> <tr v-bind="api.getTableRowProps({ view: 'day' })"> <template v-for="(day) in api.weekDays"> <th scope="col"> {{day.narrow}} </th> </template> </tr> </thead> <tbody v-bind="api.getTableBodyProps({ view: 'day' })"> <template v-for="(week) in api.weeks"> <tr v-bind="api.getTableRowProps({ view: 'day' })"> <template v-for="(value) in week"> <td v-bind="api.getDayTableCellProps({ value })"> <div v-bind="api.getDayTableCellTriggerProps({ value })"> {{value.day}} </div> </td> </template> </tr> </template> </tbody> </table> </div> <!-- Month View --> <div v-show="api.view !== 'month'"> <div v-bind="api.getViewControlProps({ view: 'month' })"> <button v-bind="api.getPrevTriggerProps({ view: 'month' })"> Prev </button> <button v-bind="api.getViewTriggerProps({ view: 'month' })"> {{ api.visibleRange.start.year }} </button> <button v-bind="api.getNextTriggerProps({ view: 'month' })"> Next </button> </div> <table v-bind="api.getTableProps({ view: 'month', columns: 4 })"> <tbody v-bind="api.getTableBodyProps({ view: 'month' })"> <template v-for="(months) in api.getMonthsGrid({ columns: 4, format: 'short' })"> <tr v-bind="api.getTableRowProps()"> <template v-for="(month) in months"> <td v-bind="api.getMonthTableCellProps({ ...month, columns: 4, })"> <div v-bind="api.getMonthTableCellTriggerProps({ ...month, columns: 4, })"> {{ month.label }} </div> </td> </template> </tr> </template> </tbody> </table> </div> <!-- Year View --> <div v-show="api.view !== 'year'"> <div v-bind="api.getViewControlProps({ view: 'year' })"> <button v-bind="api.getPrevTriggerProps({ view: 'year' })"> Prev </button> <span> {{ api.getDecade().start }} - {{ api.getDecade().end }} </span> <button v-bind="api.getNextTriggerProps({ view: 'year' })"> Next </button> </div> <table v-bind="api.getTableProps({ view: 'year', columns: 4 })"> <tbody v-bind="api.getTableBodyProps()"> <template v-for="(years) in api.getYearsGrid({ columns: 4 })"> <tr v-bind="api.getTableRowProps({ view: 'year' })"> <template v-for="(year) in years"> <td v-bind="api.getYearTableCellProps({ ...year, columns: 4, })"> <div v-bind="api.getYearTableCellTriggerProps({ ...year, columns: 4, })"> {{ year.label }} </div> </td> </template> </tr> </template> </tbody> </table> </div> </div> </div> </Teleport> </> </template>
Setting the initial date
To set the initial value that is rendered by the date picker, set the value
property in the machine context.
const [state, send] = useMachine( datepicker.machine({ value: datepicker.parse("2022-01-01"), }), )
Controlling the selected date
Use the api.setValue
method to control the selected date in the DatePicker
component.
// parse the date string into a date object const nextValue = datepicker.parse("2022-01-01") // set the new value api.setValue(nextValue)
Alternatively, you can also use the
value
andonValueChange
callbacks to programmatically control the selected date.
Controlling the open state
To manage the open state of the datepicker's dialog, we recommended using
api.setOpen
method.
// open the date picker api.setOpen(true) // close the date picker api.setOpen(false)
Alternatively, you can also use the
open
andonOpenChange
callbacks to programmatically control the open state.
Setting the min and max dates
To contrain the date range that can be selected by the user, set the min
and
max
properties in the machine context.
const [state, send] = useMachine( datepicker.machine({ min: datepicker.parse("2022-01-01"), max: datepicker.parse("2022-12-31"), }), )
When the min or max date value is reached, the next and prev triggers will be disabled.
Changing the start of the week
Set the startOfWeek
property in the machine context to change the start of the
week. The property accepts a number from 0
to 6
, where 0
is Sunday and 6
is Saturday.
const [state, send] = useMachine( datepicker.machine({ startOfWeek: 1, // Monday }), )
Disabling the date picker
To disable the date picker, set the disabled
property in the machine context
to true
.
const [state, send] = useMachine( datepicker.machine({ disabled: true, }), )
Rendering month and year pickers
To render the month and year pickers, use the api.getMonthSelectProps
and
api.getYearSelectProps
prop getters.
<div> <select {...api.getMonthSelectProps()}> {api.getMonths().map((month, i) => ( <option key={i} value={month.value}> {month.label} </option> ))} </select> <select {...api.getYearSelectProps()}> {getYearsRange({ from: 1_000, to: 4_000 }).map((year, i) => ( <option key={i} value={year}> {year} </option> ))} </select> </div>
Marking unavailable dates
To mark specific dates as unavailable, set the isDateUnavailable
function in
the machine context. This function should return true
for dates that are
unavailable.
const [state, send] = useMachine( datepicker.machine({ isDateUnavailable: (date, locale) => { // mark weekends as unavailable return date.day === 0 || date.day === 6 }, }), )
You can also leverage the numerous helpers from
@internationalized/date
to create more complex date availability rules.
import { isWeekend } from "@internationalized/date" const [state, send] = useMachine( datepicker.machine({ isDateUnavailable: (date, locale) => { // mark weekends as unavailable return isWeekend(date, locale) }, }), )
Setting the calendar starting view
The calendar view is set to day
by default. To change the starting view of the
calendar, set the view
property in the machine context to either day
,
month
, or year
.
const [state, send] = useMachine( datepicker.machine({ view: "month", }), )
Setting the read-only mode
Set the readOnly
property in the machine context to true
to make the date
picker read-only. This means that users can't change the selected date.
const [state, send] = useMachine( datepicker.machine({ readOnly: true, }), )
Setting the focused date
The datepicker's focused date is set to either the first selected date or today's date by default.
To change the focused date, set the focusedDate
property in the machine
context.
const [state, send] = useMachine( datepicker.machine({ focusedDate: datepicker.parse("2022-01-01"), }), )
Rendering the calendar inline
To render the calendar inline, we recommended setting the open
property to
true
and closeOnSelect
to false
.
const [state, send] = useMachine( datepicker.machine({ open: true, "open.controlled": true, closeOnSelect: false, }), )
Usage within a form
To use the date picker within a form, set the name
property in the machine
context. This property is used to identify the date picker in the form data.
const [state, send] = useMachine( datepicker.machine({ name: "date", }), )
Rendering fixed number of weeks
The datepicker's calendar will render the weeks needed to display all of the days in the month. Sometimes this can result in a jump in the UI when navigating between different sized months (e.g., February vs. March).
To ensure the calendar renders the maximum number of weeks (6), you can set the
fixedWeeks
prop to true
.
const [state, send] = useMachine( datepicker.machine({ fixedWeeks: true, }), )
Listening to date changes
To listen to date changes, use the onValueChange
callback in the machine
context.
const [state, send] = useMachine( datepicker.machine({ onValueChange(details) { // details => { value: DateValue[], valueAsString: string[], view: string } console.log("selected date:", details.valueAsString) }, }), )
Listening to view changes
When the calendar view changes by click on the view controls, the onViewChange
callback is invoked.
const [state, send] = useMachine( datepicker.machine({ onViewChange(details) { // details => { view: string } console.log("view changed to:", details.view) }, }), )
Styling guide
Earlier, we mentioned that each date-picker part has a data-part
attribute
added to them to select and style them in the DOM.
[data-scope="date-picker"][data-part="root"] { /* styles for the root part */ } [data-scope="date-picker"][data-part="input"] { /* styles for the input part */ } [data-scope="date-picker"][data-part="trigger"] { /* styles for the trigger part */ } [data-scope="date-picker"][data-part="content"] { /* styles for the input part */ }
Open State
[data-scope="date-picker"][data-part="trigger"] { &[data-state="open"] { /* styles for the open state */ } &[data-state="closed"] { /* styles for the closed state */ } }
Cell States
[data-scope="date-picker"][data-part="table-cell-trigger"] { /* styles for the cell */ &[data-selected] { /* styles for the selected date */ } &[data-focus] { /* styles for the focused date */ } &[data-disabled] { /* styles for the disabled date */ } &[data-unavailable] { /* styles for the unavailable date */ } &[data-today] { /* styles for the today date */ } &[data-weekend] { /* styles for the weekend date */ } }
Methods and Properties
Machine Context
The date picker machine exposes the following context properties:
locale
string
The locale (BCP 47 language tag) to use when formatting the date.translations
IntlTranslations
The localized messages to use.ids
Partial<{ root: string; label: string; table(id: string): string; tableHeader(id: string): string; tableBody(id: string): string; tableRow(id: string): string; content: string; cellTrigger(id: string): string; prevTrigger(view: DateView): string; clearTrigger: string; control: string; input(index: number): string; trigger: string; monthSelect: string; yearSelect: string; positioner: string; }>
The ids of the elements in the date picker. Useful for composition.name
string
The `name` attribute of the input element.timeZone
string
The time zone to usedisabled
boolean
Whether the calendar is disabled.readOnly
boolean
Whether the calendar is read-only.min
DateValue
The minimum date that can be selected.max
DateValue
The maximum date that can be selected.closeOnSelect
boolean
Whether the calendar should close after the date selection is complete. This is ignored when the selection mode is `multiple`.value
DateValue[]
The selected date(s).focusedValue
DateValue
The focused date.numOfMonths
number
The number of months to display.startOfWeek
number
The first day of the week. `0` - Sunday `1` - Monday `2` - Tuesday `3` - Wednesday `4` - Thursday `5` - Friday `6` - SaturdayfixedWeeks
boolean
Whether the calendar should have a fixed number of weeks. This renders the calendar with 6 weeks instead of 5 or 6.onValueChange
(details: ValueChangeDetails) => void
Function called when the value changes.onFocusChange
(details: FocusChangeDetails) => void
Function called when the focused date changes.onViewChange
(details: ViewChangeDetails) => void
Function called when the view changes.onOpenChange
(details: OpenChangeDetails) => void
Function called when the calendar opens or closes.isDateUnavailable
(date: DateValue, locale: string) => boolean
Returns whether a date of the calendar is available.selectionMode
SelectionMode
The selection mode of the calendar. - `single` - only one date can be selected - `multiple` - multiple dates can be selected - `range` - a range of dates can be selectedformat
(date: DateValue) => string
The format of the date to display in the input.view
DateView
The view of the calendarmodal
boolean
Whether the calendar should be modal. This means that the calendar will block interaction with the rest of the page, and trap focus within it.positioning
PositioningOptions
The user provided options used to position the date picker contentopen
boolean
Whether the datepicker is openopen.controlled
boolean
Whether the datepicker open state is controlled by the userdir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The date picker api
exposes the following methods:
focused
boolean
Whether the input is focusedopen
boolean
Whether the date picker is openview
DateView
The current view of the date pickergetDaysInWeek
(week: number, from?: DateValue) => DateValue[]
Returns an array of days in the week index counted from the provided start date, or the first visible date if not given.getOffset
(duration: DateDuration) => DateValueOffset
Returns the offset of the month based on the provided number of months.getRangePresetValue
(value: DateRangePreset) => DateValue[]
Returns the range of dates based on the provided date range preset.getMonthWeeks
(from?: DateValue) => DateValue[][]
Returns the weeks of the month from the provided date. Represented as an array of arrays of dates.isUnavailable
(date: DateValue) => boolean
Returns whether the provided date is available (or can be selected)weeks
DateValue[][]
The weeks of the month. Represented as an array of arrays of dates.weekDays
WeekDay[]
The days of the week. Represented as an array of strings.visibleRange
VisibleRange
The visible range of dates.visibleRangeText
VisibleRangeText
The human readable text for the visible range of dates.value
DateValue[]
The selected date.valueAsDate
Date[]
The selected date as a Date object.valueAsString
string[]
The selected date as a string.focusedValue
DateValue
The focused date.focusedValueAsDate
Date
The focused date as a Date object.focusedValueAsString
string
The focused date as a string.selectToday
() => void
Sets the selected date to today.setValue
(values: CalendarDate[]) => void
Sets the selected date to the given date.setFocusedValue
(value: CalendarDate) => void
Sets the focused date to the given date.clearValue
() => void
Clears the selected date(s).setOpen
(open: boolean) => void
Function to open or close the calendar.focusMonth
(month: number) => void
Function to set the selected month.focusYear
(year: number) => void
Function to set the selected year.getYears
() => Cell[]
Returns the months of the yeargetYearsGrid
(props?: YearGridProps) => YearGridValue
Returns the years of the decade based on the columns. Represented as an array of arrays of years.getDecade
() => Range<number>
Returns the start and end years of the decade.getMonths
(props?: MonthFormatOptions) => Cell[]
Returns the months of the yeargetMonthsGrid
(props?: MonthGridProps) => MonthGridValue
Returns the months of the year based on the columns. Represented as an array of arrays of months.format
(value: CalendarDate, opts?: Intl.DateTimeFormatOptions) => string
Formats the given date value based on the provided options.setView
(view: DateView) => void
Sets the view of the date picker.goToNext
() => void
Goes to the next month/year/decade.goToPrev
() => void
Goes to the previous month/year/decade.getDayTableCellState
(props: DayTableCellProps) => DayTableCellState
Returns the state details for a given cell.getMonthTableCellState
(props: TableCellProps) => TableCellState
Returns the state details for a given month cell.getYearTableCellState
(props: TableCellProps) => TableCellState
Returns the state details for a given year cell.
Edit this page on GitHub