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.jsonimport {
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.
'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
'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.
'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.
'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
'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
'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/2021and10/21/2021are 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'andaria-expanded, and the surface is adialog.
API Reference
DatePicker
| Prop | Type | Default | Description |
|---|---|---|---|
value | Date | null | — | Current date |
onValueChange | (date: Date) => void | — | Fires when a day is chosen |
trigger | 'field' | 'icon' | 'field' | Which trigger to render |
placeholder | string | 'Select Date' | Field text while empty |
format | (date: Date) => string | 21 Oct 2021 | Overrides the trigger text |
showViewControls | boolean | true | Month and year selects inside the surface |
min / max | Date | — | Selectable bounds |
disabled | boolean | false | Disables 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).