Skip to main content
0.75.0
View Zag.js on Github
Join the Discord server

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

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

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 the useMachine 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> </> ) }

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 and onValueChange 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 and onOpenChange 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:

  • localestringThe locale (BCP 47 language tag) to use when formatting the date.
  • translationsIntlTranslationsThe localized messages to use.
  • idsPartial<{ root: string; label(index: number): 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.
  • namestringThe `name` attribute of the input element.
  • timeZonestringThe time zone to use
  • disabledbooleanWhether the calendar is disabled.
  • readOnlybooleanWhether the calendar is read-only.
  • minDateValueThe minimum date that can be selected.
  • maxDateValueThe maximum date that can be selected.
  • closeOnSelectbooleanWhether the calendar should close after the date selection is complete. This is ignored when the selection mode is `multiple`.
  • valueDateValue[]The selected date(s).
  • focusedValueDateValueThe focused date.
  • numOfMonthsnumberThe number of months to display.
  • startOfWeeknumberThe first day of the week. `0` - Sunday `1` - Monday `2` - Tuesday `3` - Wednesday `4` - Thursday `5` - Friday `6` - Saturday
  • fixedWeeksbooleanWhether the calendar should have a fixed number of weeks. This renders the calendar with 6 weeks instead of 5 or 6.
  • onValueChange(details: ValueChangeDetails) => voidFunction called when the value changes.
  • onFocusChange(details: FocusChangeDetails) => voidFunction called when the focused date changes.
  • onViewChange(details: ViewChangeDetails) => voidFunction called when the view changes.
  • onOpenChange(details: OpenChangeDetails) => voidFunction called when the calendar opens or closes.
  • isDateUnavailable(date: DateValue, locale: string) => booleanReturns whether a date of the calendar is available.
  • selectionModeSelectionModeThe 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 selected
  • format(date: DateValue) => stringThe format of the date to display in the input.
  • viewDateViewThe view of the calendar
  • modalbooleanWhether the calendar should be modal. This means that the calendar will block interaction with the rest of the page, and trap focus within it.
  • positioningPositioningOptionsThe user provided options used to position the date picker content
  • openbooleanWhether the datepicker is open
  • open.controlledbooleanWhether the datepicker open state is controlled by the user
  • 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 picker api exposes the following methods:

  • focusedbooleanWhether the input is focused
  • openbooleanWhether the date picker is open
  • viewDateViewThe current view of the date picker
  • getDaysInWeek(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) => DateValueOffsetReturns 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) => booleanReturns whether the provided date is available (or can be selected)
  • weeksDateValue[][]The weeks of the month. Represented as an array of arrays of dates.
  • weekDaysWeekDay[]The days of the week. Represented as an array of strings.
  • visibleRangeVisibleRangeThe visible range of dates.
  • visibleRangeTextVisibleRangeTextThe human readable text for the visible range of dates.
  • valueDateValue[]The selected date.
  • valueAsDateDate[]The selected date as a Date object.
  • valueAsStringstring[]The selected date as a string.
  • focusedValueDateValueThe focused date.
  • focusedValueAsDateDateThe focused date as a Date object.
  • focusedValueAsStringstringThe focused date as a string.
  • selectToday() => voidSets the selected date to today.
  • setValue(values: CalendarDate[]) => voidSets the selected date to the given date.
  • setFocusedValue(value: CalendarDate) => voidSets the focused date to the given date.
  • clearValue() => voidClears the selected date(s).
  • setOpen(open: boolean) => voidFunction to open or close the calendar.
  • focusMonth(month: number) => voidFunction to set the selected month.
  • focusYear(year: number) => voidFunction to set the selected year.
  • getYears() => Cell[]Returns the months of the year
  • getYearsGrid(props?: YearGridProps) => YearGridValueReturns 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 year
  • getMonthsGrid(props?: MonthGridProps) => MonthGridValueReturns the months of the year based on the columns. Represented as an array of arrays of months.
  • format(value: CalendarDate, opts?: Intl.DateTimeFormatOptions) => stringFormats the given date value based on the provided options.
  • setView(view: DateView) => voidSets the view of the date picker.
  • goToNext() => voidGoes to the next month/year/decade.
  • goToPrev() => voidGoes to the previous month/year/decade.
  • getDayTableCellState(props: DayTableCellProps) => DayTableCellStateReturns the state details for a given cell.
  • getMonthTableCellState(props: TableCellProps) => TableCellStateReturns the state details for a given month cell.
  • getYearTableCellState(props: TableCellProps) => TableCellStateReturns the state details for a given year cell.

Edit this page on GitHub

Proudly made in🇳🇬by Segun Adebayo

Copyright © 2024
On this page