Date Picker
Preline-style single-date picker with integrated segmented field, calendar icon, and Radix Popover with plain Date values.
Date Picker combines a native-like segmented field (React Aria year/month/day slots) and a calendar icon inside one bordered shell, with a VCP-powered Calendar in a Preline-inspired popover. Values stay plain Date.
Interactions
| Event | Behavior |
|---|---|
| Calendar trigger click | Toggle popover |
| Focus / Tab into date segments | Does not open popover (type to edit; open via calendar icon) |
| Edit segments while popover is open | Popover stays open; calendar month and selection sync to the typed date |
| Select day (no time) | Commit date, close popover |
Select day (withTime) | Update draft date; popover stays open for time |
| Scroll / click time spinner | Update draft time; selected date preserved |
| Edit time segments in field | Update value (or draft while popover is open) |
Click Done (withTime) | Commit and close popover |
Click Today (withTime) | Set draft to today (keeps current time when already set) |
| Outside click / Escape | Close popover (withTime commits draft on close) |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/date-picker.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/date-picker.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/date-picker.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/date-picker.jsonOr with a namespace: npx shadcn@latest add @f-ui/date-picker.
Usage
import { DatePicker } from "@/components/f-ui/date-picker/date-picker";
<DatePicker
label="Date"
value={date}
onChange={setDate}
showToday
/>;Examples
Single Date
Default Preline-style picker with optional description and Today footer action.
Choose a day for your booking.
Selected: —
"use client";
import { useState } from "react";
import { DatePicker } from "@/components/f-ui/date-picker/date-picker";
export function DatePickerDemo() {
const [date, setDate] = useState<Date | undefined>(undefined);
return (
<div className="max-w-sm space-y-3">
<DatePicker
label="Date"
value={date ?? null}
onChange={setDate}
description="Choose a day for your booking."
showToday
/>
<p className="text-muted-foreground text-xs">
Selected:{" "}
<span className="text-foreground font-medium tabular-nums">
{date ? date.toLocaleDateString() : "—"}
</span>
</p>
</div>
);
}Single Date + Time
When withTime is set, a compact scroll time spinner appears below the calendar (about three visible rows); picking a date preserves the chosen time. Choose a date on the calendar, set the time in the spinner below (or type in the field), then click Done — or click outside to apply and close.
For a taller spinner, pass timeOptions={{ density: "default" }}. In tight panels, combine with calendarPopover={{ side: "top" }}.
Selected: —
"use client";
import { useState } from "react";
import { DatePicker } from "@/components/f-ui/date-picker/date-picker";
export function DatePickerWithTimeDemo() {
const [date, setDate] = useState<Date | undefined>(undefined);
return (
<div className="max-w-sm space-y-3">
<DatePicker
label="Date and time"
value={date ?? null}
onChange={setDate}
withTime
/>
<p className="text-muted-foreground text-xs">
Selected:{" "}
<span className="text-foreground font-medium tabular-nums">
{date ? date.toLocaleString() : "—"}
</span>
</p>
</div>
);
}Constrained Date
minValue, maxValue, and isDateUnavailable bound both the segmented input and calendar selection.
Weekdays only, within the next 3 months.
Selected: —
"use client";
import { useState } from "react";
import { DatePicker } from "@/components/f-ui/date-picker/date-picker";
const now = new Date();
const minDate = new Date(now.getFullYear(), now.getMonth(), 1);
const maxDate = new Date(now.getFullYear(), now.getMonth() + 3, 0);
function isWeekend(date: Date) {
const day = date.getDay();
return day === 0 || day === 6;
}
export function DatePickerConstrainedDemo() {
const [date, setDate] = useState<Date | undefined>(undefined);
return (
<div className="max-w-sm space-y-3">
<DatePicker
label="Appointment"
value={date ?? null}
onChange={setDate}
minValue={minDate}
maxValue={maxDate}
isDateUnavailable={isWeekend}
description="Weekdays only, within the next 3 months."
/>
<p className="text-muted-foreground text-xs">
Selected:{" "}
<span className="text-foreground font-medium tabular-nums">
{date ? date.toLocaleDateString() : "—"}
</span>
</p>
</div>
);
}Inside a Dialog
Use the picker inside shadcn Dialog for forms and wizards. The calendar popover defaults above the dialog layer (z-[60] over Dialog z-50). Opening the dialog focuses the field for typing without auto-opening the calendar — use the calendar icon to pick.
Selected: —
"use client";
import { useState } from "react";
import { DatePicker } from "@/components/f-ui/date-picker/date-picker";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function DatePickerDialogDemo() {
const [date, setDate] = useState<Date | undefined>(undefined);
return (
<div className="max-w-sm space-y-3">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Pick date in dialog</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Schedule date</DialogTitle>
<DialogDescription>
Pick a date inside this dialog and continue.
</DialogDescription>
</DialogHeader>
<DatePicker
label="Date"
value={date ?? null}
onChange={setDate}
showToday
/>
</DialogContent>
</Dialog>
<p className="text-muted-foreground text-xs">
Selected:{" "}
<span className="text-foreground font-medium tabular-nums">
{date ? date.toLocaleDateString() : "—"}
</span>
</p>
</div>
);
}Headless Usage
useDatePicker is the view-model: open/close, fieldProps, and calendarProps. The demo uses a native <input> + <button> for field chrome — not DatePickerControl or DatePickerTrigger. Wire calendarProps into Calendar inside DatePickerPopover.
Committed value: —
"use client";
import { useState } from "react";
import { Calendar } from "@/components/f-ui/calendar/calendar";
import { DatePickerPopover } from "@/components/f-ui/date-picker/date-picker-parts/popover";
import { useDatePickerI18n } from "@/components/f-ui/date-picker/hooks/use-date-picker-i18n";
import { useDatePicker } from "@/components/f-ui/date-picker/use-date-picker";
import { Popover, PopoverAnchor } from "@/components/ui/popover";
export function DatePickerHeadlessDemo() {
const [date, setDate] = useState<Date | undefined>(undefined);
const { t, locale } = useDatePickerI18n();
const picker = useDatePicker({
value: date ?? null,
onChange: setDate,
locale,
});
const committedLabel = date ? date.toLocaleDateString(locale) : "—";
const fieldDisplay = date ? date.toLocaleDateString(locale) : "";
return (
<Popover open={picker.isOpen} onOpenChange={picker.onOpenChange}>
<div>
<label htmlFor="headless-date-picker">Date</label>
<PopoverAnchor asChild>
<div id="headless-date-picker">
<input
type="text"
readOnly
value={fieldDisplay}
onFocus={picker.fieldProps.onFocus}
onBlur={picker.fieldProps.onBlur}
aria-label={t("ariaLabel")}
/>
<button type="button" onClick={picker.onTriggerClick} aria-label={t("openCalendar")}>
Calendar
</button>
</div>
</PopoverAnchor>
<p>Committed value: {committedLabel}</p>
</div>
<DatePickerPopover>
<Calendar {...picker.calendarProps} showToday />
</DatePickerPopover>
</Popover>
);
}You can also compose from lower-level pieces without the hook — manage open state yourself and close on day pick:
Committed value: —
"use client";
import { useState } from "react";
import { Calendar } from "@/components/f-ui/calendar/calendar";
import { DatePickerControl } from "@/components/f-ui/date-picker/date-picker-parts/control";
import { DatePickerPopover } from "@/components/f-ui/date-picker/date-picker-parts/popover";
import { Popover, PopoverAnchor, PopoverTrigger } from "@/components/ui/popover";
export function DatePickerHtmlDemo() {
const [date, setDate] = useState<Date | undefined>(undefined);
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<div className="max-w-sm">
<label htmlFor="html-date-picker">Date</label>
<PopoverAnchor asChild>
<div id="html-date-picker" className="mt-1 min-w-0">
<DatePickerControl
value={date ?? null}
onChange={setDate}
aria-label="Date"
calendarTrigger={
<PopoverTrigger asChild>
<button type="button" onClick={() => setOpen((prev) => !prev)}>
Calendar
</button>
</PopoverTrigger>
}
/>
</div>
</PopoverAnchor>
<p className="mt-2 text-sm">
Committed value:{" "}
<strong>{date ? date.toLocaleDateString() : "—"}</strong>
</p>
</div>
<DatePickerPopover className="w-fit max-w-[min(100vw-2rem,100%)] overflow-hidden p-0">
<Calendar
mode="single"
value={date ?? null}
onChange={(next) => {
setDate(next);
setOpen(false);
}}
showToday
classNames={{ root: "border-0 bg-transparent shadow-none" }}
/>
</DatePickerPopover>
</Popover>
);
}Composition
DatePicker
├── Label (optional)
├── DatePickerControl (segmented field + calendar icon)
│ └── PopoverTrigger → DatePickerTrigger
├── DatePickerPopover
│ └── Calendar mode="single"
└── Description / Error messageAPI Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | Date | null | — | — |
onChange | (date: Date | undefined) => void | — | — |
onBlur | () => void | — | — |
locale | string | i18n/provider locale | — |
withTime | boolean | false | — |
timeOptions | { format?: string; use12Hours?: boolean; step?: { hour?; minute?; second? }; density?: "default" | "compact" } | — | Configures the in-popover time spinner when withTime is set. density defaults to "compact" in the calendar popover; pass "default" for the full standalone TimePicker height. Also supports 12-hour (use12Hours), seconds (format: "HH:mm:ss"), and stepped columns (step). |
minValue / maxValue | Date | — | — |
isDateUnavailable | (date: Date) => boolean | — | |
firstDayOfWeek | 0 | 1 | 2 | 3 | 4 | 5 | 6 | locale default | — |
disabled | boolean | false | — |
isInvalid | boolean | false | — |
label | ReactNode | — | — |
aria-label | string | localized ariaLabel | — |
description | ReactNode | — | — |
errorMessage | ReactNode | — | — |
showToday | boolean | calendar default | — |
className | string | — | — |
classNames | Partial<Record<DatePickerSlot, string>> | — | — |
t | DatePickerTranslateFn | built-in/provider | — |
Slots
| Slot | Element |
|---|---|
root | Outer wrapper |
field | Field wrapper |
input | Segmented date input inside control |
icon | Calendar trigger button |
popover | Popover panel |
calendar | Embedded Calendar |
description | Description text |
errorMessage | Error text |
Hook — useDatePicker(options)
| Option | Type |
|---|---|
value | Date | null |
onChange | (date: Date | undefined) => void |
onBlur | () => void |
locale | string (required) |
withTime | boolean |
minValue / maxValue | Date |
isDateUnavailable | (date: Date) => boolean |
firstDayOfWeek | 0–6 |
disabled | boolean |
isInvalid | boolean |
| Return | Type |
|---|---|
value | committed Date |
isOpen / setIsOpen | popover open state |
onOpenChange | (open: boolean) => void |
onTriggerClick | toggle handler for your trigger button |
fieldProps | spread on DatePickerControl (omit calendarTrigger) |
calendarProps | spread on Calendar (mode: "single") |