A button that opens a calendar popover to choose one date, with presets, month and year selects, min and max dates, clearing, invalid state and form submission.
We deliver Monday to Friday.
"use client";
import * as React from "react";
import { DatePicker, type DatePickerPreset } from "@/components/ballmac/date-picker";
import { Field, FieldDescription, FieldLabel, useFieldControl } from "@/components/ballmac/field";
function Picker({ presets }: { presets: DatePickerPreset[] }) {
const { id, "aria-describedby": describedby } = useFieldControl();
return (
<DatePicker
id={id}
aria-describedby={describedby}
aria-label="Delivery date"
defaultValue={new Date(2026, 9, 14)}
presets={presets}
clearable
minDate={new Date(2026, 8, 1)}
/>
);
}
export default function DatePickerDemo() {
const presets = React.useMemo<DatePickerPreset[]>(() => {
const now = new Date();
const at = (d: number) => new Date(now.getFullYear(), now.getMonth(), now.getDate() + d);
return [
{ label: "Today", date: at(0) },
{ label: "Tomorrow", date: at(1) },
{ label: "In a week", date: at(7) },
{ label: "In a month", date: new Date(now.getFullYear(), now.getMonth() + 1, now.getDate()) },
];
}, []);
return (
<Field className="w-full max-w-xs">
<FieldLabel>Delivery date</FieldLabel>
<Picker presets={presets} />
<FieldDescription>We deliver Monday to Friday.</FieldDescription>
</Field>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/date-pickerInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/calendar @ballmac/popover @ballmac/i18nCopy the source into your project.
components/ballmac/date-picker.tsx// Ballmac UI: Date Picker. https://ui.ballmac.com/components/date-picker "use client"; import * as React from "react"; import { CalendarDays, X } from "lucide-react"; import { Calendar } from "@/components/ballmac/calendar"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ballmac/popover"; import { cn } from "@/lib/utils"; import { useLocale, useMessages } from "@/lib/ballmac/i18n"; type DatePickerPreset = { /** Button label, for example "Tomorrow". */ label: string; /** The date the preset selects. Build it with `new Date(y, m, d)` so it is local midnight. */ date: Date; }; type DatePickerProps = Omit< React.ComponentProps<"button">, "value" | "defaultValue" | "onChange" > & { /** Controlled date. Pass `null` for a controlled empty value; `undefined` leaves the picker uncontrolled. */ value?: Date | null; /** Initial date when uncontrolled. */ defaultValue?: Date; /** Called with the new date, or `undefined` when cleared. */ onValueChange?: (date: Date | undefined) => void; /** Text on the trigger when no date is chosen. */ placeholder?: string; /** Locale for the trigger text and calendar. The default keeps server and browser output identical. */ locale?: string; /** Formatting options for the trigger text. */ format?: Intl.DateTimeFormatOptions; /** Earliest selectable date. */ minDate?: Date; /** Latest selectable date. */ maxDate?: Date; /** Quick picks shown beside the calendar. */ presets?: DatePickerPreset[]; /** Show a control that clears the date. */ clearable?: boolean; /** Mark the field invalid (`aria-invalid`, destructive border). */ invalid?: boolean; /** Name of a hidden input that submits the date as YYYY-MM-DD. */ name?: string; /** Show month and year selects in the calendar header. */ dropdowns?: boolean; }; function toIso(d: Date) { const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${d.getFullYear()}-${m}-${day}`; } const sameDay = (a?: Date, b?: Date) => !!a && !!b && toIso(a) === toIso(b); /** A button that opens a calendar popover to choose one date, with optional presets and form submission. */ function DatePicker({ value: valueProp, defaultValue, onValueChange, placeholder, locale, format = { dateStyle: "medium" }, minDate, maxDate, presets, clearable = false, invalid = false, name, dropdowns = false, disabled, className, "aria-label": ariaLabel, ...props }: DatePickerProps) { const defaultLocale = useLocale() locale ??= defaultLocale const msg = useMessages() placeholder ??= msg("date-picker.placeholder", "Pick a date") const [open, setOpen] = React.useState(false); const [inner, setInner] = React.useState<Date | undefined>(defaultValue); const value = valueProp === undefined ? inner : (valueProp ?? undefined); const choose = (next: Date | undefined) => { setInner(next); onValueChange?.(next); if (next) setOpen(false); }; const text = value ? new Intl.DateTimeFormat(locale, format).format(value) : placeholder; return ( <Popover open={open} onOpenChange={setOpen}> <div className="relative w-full"> <PopoverTrigger data-slot="date-picker" disabled={disabled} aria-invalid={invalid || undefined} aria-label={ariaLabel ? `${ariaLabel}${value ? `, ${text}` : ""}` : undefined} className={cn( "flex h-9 w-full min-w-0 items-center gap-2 rounded-md border border-input bg-background px-3 text-start text-sm shadow-xs outline-none transition-[color,border-color,box-shadow] duration-150 hover:bg-accent/40 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30", !value && "text-muted-foreground", clearable && value && "pe-10", className, )} {...props} > <CalendarDays aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" /> <span className="truncate">{text}</span> </PopoverTrigger> {clearable && value && !disabled && ( <button type="button" aria-label={msg("date-picker.clearDate", "Clear date")} onClick={() => choose(undefined)} className="absolute top-1/2 end-2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50" > <X aria-hidden="true" className="size-3.5" /> </button> )} {name && <input type="hidden" name={name} value={value ? toIso(value) : ""} />} </div> <PopoverContent label={ariaLabel ?? placeholder} align="start" className="w-auto max-w-[calc(100vw-1.5rem)] gap-0 p-0" > <div className="flex flex-col sm:flex-row"> {presets && presets.length > 0 && ( <div className="flex flex-wrap gap-1.5 border-b p-3 sm:w-36 sm:flex-col sm:flex-nowrap sm:border-e sm:border-b-0"> {presets.map((p) => ( <button key={p.label} type="button" aria-pressed={sameDay(p.date, value)} onClick={() => choose(p.date)} className="h-8 rounded-md px-2.5 text-start text-sm outline-none transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-pressed:bg-accent aria-pressed:font-medium" > {p.label} </button> ))} </div> )} <Calendar className="p-3" mode="single" selected={value} defaultMonth={value} onSelect={choose} disabled={[...(minDate ? [{ before: minDate }] : []), ...(maxDate ? [{ after: maxDate }] : [])]} captionLayout={dropdowns ? "dropdown" : "label"} startMonth={dropdowns ? (minDate ?? new Date(new Date().getFullYear() - 100, 0)) : undefined} endMonth={dropdowns ? (maxDate ?? new Date(new Date().getFullYear() + 10, 11)) : undefined} /> </div> </PopoverContent> </Popover> ); } export { DatePicker, type DatePickerProps, type DatePickerPreset };Update the import paths to match your project setup.
Usage
import { DatePicker } from "@/components/ballmac/date-picker"The full example is in the Code tab above.
Examples
Birthday, invalid, disabled
"use client";
import { DatePicker } from "@/components/ballmac/date-picker";
import { Field, FieldError, FieldLabel } from "@/components/ballmac/field";
export default function DatePickerStates() {
return (
<div className="grid w-full max-w-xs gap-4">
<Field>
<FieldLabel>Date of birth</FieldLabel>
<DatePicker
aria-label="Date of birth"
placeholder="Select your birthday"
dropdowns
maxDate={new Date()}
format={{ year: "numeric", month: "long", day: "numeric" }}
/>
</Field>
<Field invalid>
<FieldLabel required>Start date</FieldLabel>
<DatePicker aria-label="Start date" invalid placeholder="Required" />
<FieldError errors={["Choose a start date to continue."]} />
</Field>
<DatePicker aria-label="Locked date" disabled defaultValue={new Date(2026, 8, 30)} />
</div>
);
}API reference
| Prop | Type | Default |
|---|---|---|
valueControlled date. Pass `null` for a controlled empty value; `undefined` leaves the picker uncontrolled. | Date | null | — |
defaultValueInitial date when uncontrolled. | Date | — |
onValueChangeCalled with the new date, or `undefined` when cleared. | (date: Date | undefined) => void | — |
placeholderText on the trigger when no date is chosen. | string | — |
localeLocale for the trigger text and calendar. The default keeps server and browser output identical. | string | — |
formatFormatting options for the trigger text. | Intl.DateTimeFormatOptions | { dateStyle: "medium" } |
minDateEarliest selectable date. | Date | — |
maxDateLatest selectable date. | Date | — |
presetsQuick picks shown beside the calendar. | DatePickerPreset[] | — |
clearableShow a control that clears the date. | boolean | false |
invalidMark the field invalid (`aria-invalid`, destructive border). | boolean | false |
nameName of a hidden input that submits the date as YYYY-MM-DD. | string | — |
dropdownsShow month and year selects in the calendar header. | boolean | false |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Enter / Space / ArrowDown | Opens the calendar from the trigger |
| Arrow keys | Move between days |
| Escape | Closes and returns focus to the trigger |
Use with AI
Give it a defaultValue or value and onValueChange. presets add quick picks, dropdowns adds month and year selects, name submits YYYY-MM-DD. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Date Picker (@ballmac/date-picker) to this project with the shadcn MCP, then use it where it fits.
Use it for
- A single date in a form
- Deadlines, birthdays and delivery dates
- When quick picks like Tomorrow help
Not for
- A date range; use date-range-picker
- An always-visible calendar; use calendar
Registry JSON: https://ui.ballmac.com/r/date-picker.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
Pairs well with
Field
Form field layout with label, description and error that wire their ids to the control automatically, plus fieldset, legend, orientation and invalid/disabled state.
Calendar
A date grid for one day, several days or a range, with month and year selects, week numbers, disabled rules and range-end styling, on React DayPicker.
Popover
A focus-managed compact surface for controls or details, with collision handling, mobile-safe width, and optional close action.
Color Picker
A native color chooser paired with an editable hex field and a live swatch.