IndiaCN UI

Date Picker

Date, range, time and date-time pickers behind a field or icon trigger.

Introduction

Four pickers over one Calendar and one Time Picker: a single date, a span across two months, a time of day, and both on one surface.

UX4G gives every picker two triggers — an input-shaped field with a trailing icon, or a bare 32px icon button. Both are here as trigger='field' and trigger='icon'.

The field is a button, not a text input. It looks identical to the kit's field, but a real input invites typing and then has to guess whether "03/04" is March or April. The button says what it is — aria-haspopup='dialog', the chosen date as its accessible name — and the grid stays the one way in.

Installation

npx shadcn@latest add https://indiacn.in/r/date-picker.json
import {
DatePicker,
DateRangePicker,
DateTimePicker,
TimePickerField,
} from '@/components/ui/date-picker';

Usage

const [date, setDate] = useState<Date | null>(null);

return <DatePicker value={date} onValueChange={setDate} />;

Examples

Default

Picking a day commits it and closes the surface — there is nothing left to confirm.

DatePickerDefault
'use client';

import { useState } from 'react';

import { DatePicker } from '@/components/ui/date-picker';

export default function Component() {
  const [date, setDate] = useState<Date | null>(null);

  return <DatePicker value={date} onValueChange={setDate} />;
}

Triggers

DatePickerTriggers
'use client';

import { useState } from 'react';

import { IDateRange } from '@/components/ui/calendar';
import {
  DatePicker,
  DateRangePicker,
  DateTimePicker,
  TimePickerField,
} from '@/components/ui/date-picker';
import { ITimeValue } from '@/components/ui/time-picker';
import { Label1 } from '@/components/ui/typography';

export function DatePickerDefault() {
  const [date, setDate] = useState<Date | null>(null);

  return (
    <div className='w-full max-w-[320px]'>
      <DatePicker value={date} onValueChange={setDate} />
    </div>
  );
}

export function DatePickerTriggers() {
  const [field, setField] = useState<Date | null>(null);
  const [icon, setIcon] = useState<Date | null>(null);

  return (
    <div className='flex w-full max-w-[420px] items-end gap-4'>
      <div className='flex-1'>
        <DatePicker value={field} onValueChange={setField} />
      </div>
      <DatePicker value={icon} onValueChange={setIcon} trigger='icon' />
    </div>
  );
}

export function DateRangePickerDefault() {
  const [range, setRange] = useState<IDateRange>({ from: null, to: null });

  return (
    <div className='w-full max-w-[360px]'>
      <DateRangePicker value={range} onValueChange={setRange} />
    </div>
  );
}

export function TimePickerFieldDefault() {
  const [time, setTime] = useState<ITimeValue | null>(null);

  return (
    <div className='w-full max-w-[320px]'>
      <TimePickerField value={time} onValueChange={setTime} />
    </div>
  );
}

export function DateTimePickerDefault() {
  const [value, setValue] = useState<Date | null>(null);

  return (
    <div className='flex w-full max-w-[360px] flex-col gap-2'>
      <Label1 className='text-neutral'>Appointment</Label1>
      <DateTimePicker value={value} onValueChange={setValue} showSeconds={false} />
    </div>
  );
}

export function DateTimePicker12Hour() {
  const [value, setValue] = useState<Date | null>(null);

  return (
    <div className='w-full max-w-[360px]'>
      <DateTimePicker value={value} onValueChange={setValue} hourCycle={12} showSeconds={false} />
    </div>
  );
}

Range

The surface stays open until both ends are set, because closing on the first click would make the second one impossible.

DateRangePickerDefault
'use client';

import { useState } from 'react';

import { IDateRange } from '@/components/ui/calendar';
import { DateRangePicker } from '@/components/ui/date-picker';

export default function Component() {
  const [range, setRange] = useState<IDateRange>({ from: null, to: null });

  return <DateRangePicker value={range} onValueChange={setRange} />;
}

Time

Ok commits the draft, so scrolling past a value costs nothing.

TimePickerFieldDefault
'use client';

import { useState } from 'react';

import { TimePickerField } from '@/components/ui/date-picker';
import { ITimeValue } from '@/components/ui/time-picker';

export default function Component() {
  const [time, setTime] = useState<ITimeValue | null>(null);

  return <TimePickerField value={time} onValueChange={setTime} />;
}

Date and time

Appointment
DateTimePickerDefault
'use client';

import { useState } from 'react';

import { DateTimePicker } from '@/components/ui/date-picker';

export default function Component() {
  const [value, setValue] = useState<Date | null>(null);

  return <DateTimePicker value={value} onValueChange={setValue} showSeconds={false} />;
}

Twelve-hour clock

DateTimePicker12Hour
'use client';

import { useState } from 'react';

import { DateTimePicker } from '@/components/ui/date-picker';

export default function Component() {
  const [value, setValue] = useState<Date | null>(null);

  return (
    <DateTimePicker value={value} onValueChange={setValue} hourCycle={12} showSeconds={false} />
  );
}

Accessibility

  • The trigger's accessible name carries the current value — "Choose date: 21 Oct 2021" — so it is never just "Choose date" with the answer only visible.
  • Dates are formatted as 21 Oct 2021, with the month spelled out. 21/10/2021 and 10/21/2021 are the same eleven characters and different days.
  • The surface is a popover: Esc closes it and focus returns to the trigger.
  • Inside, the calendar keeps its full grid keyboard model and each time column its listbox model.
  • The pickers that hold a draft — time and date-time — commit only on Ok, so a keyboard user passing through values does not fire a change per keystroke.
  • Focus stays inside the surface until the choice is confirmed or cancelled. Before this was enforced, Tab from the last day cell closed the picker and dropped focus on a bare span — nowhere the eye could follow.
  • The trigger carries aria-haspopup='dialog' and aria-expanded, and the surface is a dialog.

API Reference

DatePicker

PropTypeDefaultDescription
valueDate | null—Current date
onValueChange(date: Date) => void—Fires when a day is chosen
trigger'field' | 'icon''field'Which trigger to render
placeholderstring'Select Date'Field text while empty
format(date: Date) => string21 Oct 2021Overrides the trigger text
showViewControlsbooleantrueMonth and year selects inside the surface
min / maxDate—Selectable bounds
disabledbooleanfalseDisables the trigger

DateRangePicker

Takes value: IDateRange and onValueChange: (range: IDateRange) => void, plus trigger, placeholder, min, max and disabled.

TimePickerField

Takes value: ITimeValue | null and onValueChange: (value: ITimeValue) => void, plus trigger, placeholder, showSeconds, hourCycle and disabled.

DateTimePicker

Takes value: Date | null and onValueChange: (date: Date) => void, plus trigger, placeholder, showSeconds, hourCycle, min, max and disabled. Ok commits the day and the time together.

Below the sm breakpoint the calendar and the clock stack instead of sitting side by side, so the surface fits a phone, and Today and Ok share one row.

Stacked, the surface is taller than a phone viewport, so every picker caps itself at the height Radix reports as available and scrolls inside that. Without the cap the combined picker came to 703px in an 844px viewport and hung 307px above the top edge, where it could not be reached.

The module also exports formatDate(date) and formatRange(range).

On this page