Use Date Picker when a date field should open a calendar on demand. This is a composition recipe, not a standalone registry endpoint. It keeps selected-date and popup state in your application.
"use client";
import { format } from "date-fns";
import { Calendar as CalendarIcon, ChevronDown } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export default function DatePickerDemo() {
const [date, setDate] = React.useState<Date>();
const [open, setOpen] = React.useState(false);
const id = React.useId();
const trigger = React.useRef<HTMLButtonElement>(null);
return (
<div className="w-full max-w-sm">
<div className="grid gap-2">
<label htmlFor={id} className="text-sm font-heading">
Project date
</label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
render={
<Button
id={id}
ref={trigger}
variant="neutral"
aria-describedby={`${id}-hint`}
className="w-full justify-start text-left font-base"
/>
}
>
<CalendarIcon aria-hidden="true" />
<span className="flex-1">{date ? format(date, "PPP") : "Pick a date"}</span>
<ChevronDown aria-hidden="true" />
</PopoverTrigger>
<PopoverContent
aria-label="Choose a project date"
align="start"
side="bottom"
sideOffset={8}
collisionPadding={12}
className="w-auto border-0! bg-transparent p-0 shadow-none"
>
<Calendar
mode="single"
selected={date}
defaultMonth={date}
// oxlint-disable-next-line jsx-a11y/no-autofocus -- Focus the active day only inside this user-opened popup.
autoFocus
onSelect={(selectedDate) => {
setDate(selectedDate);
if (selectedDate) setOpen(false);
}}
/>
</PopoverContent>
</Popover>
<p id={`${id}-hint`} className="text-xs leading-5 text-foreground/70">
Choose a date. Use the arrow keys to move between days.
</p>
</div>
<div className="mt-4 flex min-h-9 items-center justify-between gap-3 border-t border-border pt-3">
<output className="text-xs text-foreground/80">
{date ? `Selected: ${format(date, "PPP")}` : "No date selected"}
</output>
<Button
size="sm"
variant="ghost"
disabled={!date}
onClick={() => {
setDate(undefined);
trigger.current?.focus();
}}
>
Clear
</Button>
</div>
</div>
);
}
#Installation
Complete the base setup, then install Button, Popover, and Calendar. Copy the recipe into your application.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/button.json https://neobrutal-ui.andongmin.com/r/popover.json https://neobrutal-ui.andongmin.com/r/calendar.jsonThe live preview uses date-fns; install it when copying that preview. The standalone Usage example below uses native date formatting instead.
npm install date-fns#Usage
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export default function AppointmentDate() {
const [date, setDate] = useState<Date | undefined>();
const [open, setOpen] = useState(false);
return (
<div className="grid max-w-sm gap-2">
<label htmlFor="appointment-date">Appointment date</label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger render={<Button id="appointment-date" variant="outline" />}>
{date ? date.toLocaleDateString() : "Choose a date"}
</PopoverTrigger>
<PopoverContent aria-label="Choose an appointment date" align="start" sideOffset={8} className="w-auto border-0! bg-transparent p-0 shadow-none">
<Calendar
mode="single"
selected={date}
defaultMonth={date}
autoFocus
onSelect={(next) => {
setDate(next);
setOpen(false);
}}
/>
</PopoverContent>
</Popover>
</div>
);
}#API reference
| Recipe state / part | Purpose | Default |
|---|---|---|
date / setDate | Selected Date or undefined | No date |
open / setOpen | Popup visibility | false |
Calendar.mode | Selection mode | single here |
Calendar.disabled | Prevent selection of matching dates | None |
PopoverContent.align | Trigger-relative placement | start here |
Calendar.onSelect | Save date and optionally close | Application callback |
#Accessibility
Label the trigger and keep the chosen date visible. Verify keyboard date navigation, Escape, and focus return after closing. A button does not submit a date value automatically: add application form state or a hidden input when needed. Store date-only values without unintended timezone conversion, and distinguish navigation bounds from disabled dates.