# Date picker

> A typed date field with a calendar to choose from, and a time when needed

Status: beta. Added in Components 2.16.0.

A typed date field with a calendar to choose from, and a time when needed.

## Import

```tsx
import { DatePicker } from '@oztix/roadie-components/date-picker'
```

## Examples

### Default

Type a date, as in [DateField](https://ticketsolutionsptyltd.github.io/roadie/components/date-field.md), or press the calendar button and choose a day. Focus the empty field for hints at what can be typed, such as Tomorrow or In 2 weeks, and as you type, the first suggestion is highlighted so `Enter` takes it. The calendar opens on the chosen day, or on today, with focus on it. Choosing a day closes it and puts focus back on the button. The value is a plain ISO date such as `'2026-11-27'`, or null.

```tsx
function Default() {
  const [date, setDate] = useState(null)
  return (
    <>
      <DatePicker aria-label='Show date' value={date} onValueChange={setDate} />
      <p className='text-sm text-subtle'>Value: {date ?? 'none'}</p>
    </>
  )
}

render(<Default />)
```

### Variants

`granularity='minute'` adds a [TimeField](https://ticketsolutionsptyltd.github.io/roadie/components/time-field.md) beside the date. The value is then the instant the date and time name in `timeZone`, with its offset, such as `'2026-11-27T19:30:00+11:00'`, and it stays null until both are filled in. Pass the venue's zone, so a Perth organiser setting doors for a Melbourne show sets them on Melbourne's clock. A time that daylight saving skips, such as 2:30am on the morning the clocks go forward, moves to where it lands, 3:30am, and the field shows it.

```tsx
function Doors() {
  const [doors, setDoors] = useState('2026-11-27T19:30:00+11:00')
  return (
    <>
      <Field>
        <Field.Label>Doors, Melbourne time</Field.Label>
        <DatePicker
          granularity='minute'
          timeZone='Australia/Melbourne'
          minuteStep={15}
          value={doors}
          onValueChange={setDoors}
        />
        <Field.ErrorText />
      </Field>
      <p className='text-sm text-subtle'>Value: {doors ?? 'none'}</p>
    </>
  )
}

render(<Doors />)
```

### Emphasis

```tsx
<DatePicker aria-label='Normal' defaultValue='2026-11-27' />
<DatePicker aria-label='Subtle' emphasis='subtle' defaultValue='2026-11-27' />
```

### Sizes

The calendar button shrinks with the field, so it always sits inside it.

```tsx
<DatePicker aria-label='Small' size='sm' defaultValue='2026-11-27' />
<DatePicker aria-label='Medium' size='md' defaultValue='2026-11-27' />
<DatePicker aria-label='Large' size='lg' defaultValue='2026-11-27' />
```

### States

`readOnly` shows the date and turns off the calendar button. `disabled` turns off both.

```tsx
{/* Default */}
<DatePicker aria-label='Default' defaultValue='2026-11-27' />
{/* Invalid */}
<DatePicker aria-label='Invalid' defaultValue='2026-11-27' invalid />
{/* Read only */}
<DatePicker aria-label='Read only' defaultValue='2026-11-27' readOnly />
{/* Disabled */}
<DatePicker aria-label='Disabled' defaultValue='2026-11-27' disabled />
```

### Composition

#### With Field

Wrap it in [Field](https://ticketsolutionsptyltd.github.io/roadie/components/field.md). The label points at the date input, and `Field.ErrorText` says why typed text isn't a date or a time. At `minute` granularity the date and time sit in a group named by the label.

```tsx
<Field required className='max-w-64'>
  <Field.Label showIndicator>Show date</Field.Label>
  <DatePicker placeholder='14 Mar or next Fri' />
  <Field.HelperText>Type a date or choose one</Field.HelperText>
  <Field.ErrorText />
</Field>
```

### With disabled days

`disabled` takes the same matchers as [Calendar](https://ticketsolutionsptyltd.github.io/roadie/components/calendar.md). Matched days are disabled in the calendar and refused when typed. Here Raffia Spaceship runs on Fridays and Saturdays in November.

```tsx
<Field className='max-w-64'>
  <Field.Label>Raffia Spaceship night</Field.Label>
  <DatePicker
    disabled={[
      { before: '2026-11-01' },
      { after: '2026-11-30' },
      { dayOfWeek: [1, 2, 3, 4, 7] }
    ]}
    startMonth='2026-11-01'
    endMonth='2026-11-30'
  />
  <Field.ErrorText />
</Field>
```

### With a date of birth

`captionLayout='dropdown'` swaps the month name for month and year selects, for dates far from today. Most people will type a date of birth, but the selects help those who don't.

```tsx
<Field className='max-w-64'>
  <Field.Label>Date of birth</Field.Label>
  <DatePicker
    captionLayout='dropdown'
    startMonth='1920-01-01'
    endMonth='2012-12-31'
    disabled={{ after: '2012-12-31' }}
  />
  <Field.ErrorText />
</Field>
```

## Guidelines

### Use DateRangePicker for a range

**Do**

Choose a start and an end with [DateRangePicker](https://ticketsolutionsptyltd.github.io/roadie/components/date-range-picker.md), which keeps them in order and offers presets.

**Don’t**

Don't put two DatePickers side by side for a range. Nothing stops the end coming before the start.

### Pass the zone the moment belongs to

**Do**

```tsx
<DatePicker
  granularity='minute'
  timeZone={venue.timeZone}
/>
```

Event times are read on the venue's clock. Say so in the label, such as "Doors, Melbourne time".

**Don’t**

```tsx
<DatePicker granularity='minute' />
```

Don't lean on the browser's zone for an event time. An organiser in another state sets the wrong moment.

### Disable days, rather than hide them

**Do**

Disable days that can't be chosen. They stay in the grid for keyboard users and are refused when typed.

**Don’t**

Don't check the date only after the form is sent. Say what's wrong as soon as it's chosen.

## Accessibility

- The date is a combobox, a text input with suggestions, labelled by `Field.Label` or `aria-label`. The calendar button is named after the picker and its date, such as "Choose date, Fri 27 Nov 2026, Doors", and the popup "Choose date, Doors", so pickers side by side stay apart.
- On a screen narrower than 48rem, the calendar opens in a bottom [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md) instead of a popover. Screen readers announce a dialog with the same name. Its title is the picker's label, days grow to fill the width up to 48px, a swipe sideways over the days turns the month, and Close, `Escape`, a tap outside or a swipe down closes it.
- At `minute` granularity the date and the time are a `group` named by the label, with inputs named "Date" and "Time".
- Opening the calendar moves focus to the chosen day, or to today. Arrow keys, `Page Up`, `Page Down`, `Home` and `End` move as in [Calendar](https://ticketsolutionsptyltd.github.io/roadie/components/calendar.md). `Enter` or `Space` chooses the focused day and closes it. `Escape` closes it without a change. Either way focus goes back to the button, so on a phone the keyboard doesn't open.
- Focusing the empty date opens hints with nothing highlighted, so `Enter` still submits the form. Once you type, the first suggestion is highlighted and `Enter` takes it. The arrow keys move through the list, and `Escape` closes it and keeps the text. On a phone the list opens under the field, above the page.
- With no suggestion highlighted, `Enter` commits typed text instead of submitting the form, and `Escape` puts back the last date.
- With a `name`, a hidden input submits the value, so a form never receives the formatted text. A disabled picker submits nothing.

## API reference

### DatePicker

A typed date field with a calendar to choose from, and a time when needed.

```tsx
import { DatePicker } from '@oztix/roadie-components/date-picker'
```

- `value`: `string | null`. An ISO date such as `'2026-11-27'` at `day` granularity. At `minute`, an ISO date-time; one with an offset is shown on the wall clock of `timeZone`. Pair with `onValueChange`.
- `defaultValue`: `string | null`. The value to start from when uncontrolled.
- `onValueChange`: `((value: string | null) => void)`. Called with an ISO date at `day` granularity. At `minute`, with the instant the date and time name in `timeZone`, with its offset, such as `'2026-11-27T19:30:00+11:00'`, or null until both are known.
- `granularity`: `"day" | "minute"`. Defaults to `day`. `minute` adds a time field beside the date.
- `timeZone`: `string`. IANA zone whose wall clock the date and time are read on, and whose calendar decides today. Pass the venue's zone for an event time. Defaults to the viewer's.
- `disabled`: `boolean | CalendarMatchers`. `true` turns the picker off. Matchers, as on `Calendar`, disable those days in the calendar and refuse them when typed. Inherits `true` from `Field`.
- `invalid`: `boolean`. Marks the picker as invalid. Inherits from `Field` when omitted.
- `required`: `boolean`. Inherits from `Field` when omitted.
- `readOnly`: `boolean`. Shows the value without letting it change.
- `name`: `string`. Submits the value with a form under this name.
- `form`: `string`. The id of a form outside the picker to submit the value with.
- `size`: `"sm" | "md" | "lg"`. Defaults to `md`.
- `emphasis`: `"normal" | "subtle"`. Defaults to `'normal'`.
- `placeholder`: `string`. Hint in the empty date input, such as "14 Mar or next Fri".
- `inputRef`: `Ref<HTMLInputElement>`. A ref to the date input, for focusing it.
- `dateStyle`: `"full" | "long" | "medium"`. Defaults to `'long'`. How the date reads when not being edited, from the datetime formatters.
- `hourCycle`: `12 | 24`. Defaults to `12`. At `minute` granularity, `12` reads "7:30pm" and `24` reads "19:30".
- `minuteStep`: `number`. Defaults to `1`. At `minute` granularity, minutes per step of the time field's arrow keys. A typed time off the step shows an error.
- `today`: `string`. Today as an ISO date. Defaults to today in `timeZone`.
- `weekStart`: `number`. Defaults to `1`. First day of the week in the calendar: 1 is Monday and 7 is Sunday.
- `locale`: `string`. Defaults to `'en-AU'`.
- `captionLayout`: `"label" | "dropdown"`. Defaults to `'label'`. `dropdown` swaps the calendar's month name for month and year selects.
- `startMonth`: `string`. The earliest month the calendar reaches, as any ISO date in it.
- `endMonth`: `string`. The latest month the calendar reaches, as any ISO date in it.
- `open`: `boolean`. Whether the calendar is open. Pair with `onOpenChange`.
- `defaultOpen`: `boolean`. Defaults to `false`. Whether the calendar starts open when uncontrolled.
- `onOpenChange`: `((open: boolean) => void)`. Called when the calendar opens or closes.

Docs updated 10 Oct 2026.
