Date Picker
A date field that opens the ai2 Calendar in a Popover. Controlled or uncontrolled via value, defaultValue and onValueChange, formatted with date-fns.
"use client"
import * as React from "react"
import { DatePicker } from "@/components/ui/date-picker"
export default function DatePickerDemo() {
const [date, setDate] = React.useState<Date | undefined>()
return (
<DatePicker value={date} onValueChange={setDate} placeholder="Select a date" />
)
}Installation
Run the following command
npx shadcn@latest add @ai2/date-pickerdate-fns, the ai2 Button, Calendar and Popover, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install date-fns@^4.4.0 lucide-react@^1.23.0It also composes the ai2 Button, Calendar and Popover, so add @ai2/button, @ai2/calendar and @ai2/popover too.
Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/date-picker.tsx"use client"
import * as React from "react"
import { format, type Locale } from "date-fns"
import { Calendar as CalendarIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
interface DatePickerProps
extends Omit<React.ComponentProps<typeof Button>, "value" | "defaultValue" | "onChange"> {
value?: Date
defaultValue?: Date
onValueChange?: (date: Date | undefined) => void
placeholder?: string
/** date-fns locale for the trigger label; also forwarded to the calendar. */
locale?: Locale
/** date-fns format string for the trigger label. */
formatStr?: string
/** Extra props forwarded to the inner Calendar (e.g. disabled matchers, startMonth). */
calendarProps?: Omit<
React.ComponentProps<typeof Calendar>,
"mode" | "selected" | "onSelect"
>
}
function DatePicker(allProps: DatePickerProps) {
const {
value,
defaultValue,
onValueChange,
placeholder = "Pick a date",
locale,
formatStr = "PPP",
calendarProps,
className,
disabled,
...props
} = allProps
const [open, setOpen] = React.useState(false)
const [internal, setInternal] = React.useState<Date | undefined>(defaultValue)
// The controlled split looks at whether the prop is PRESENT: clearing with
// value={undefined} was impossible under `value ?? internal` (2026-07-11
// audit R27).
const controlled = "value" in allProps
const date = controlled ? value : internal
const select = (next: Date | undefined) => {
if (!controlled) setInternal(next)
onValueChange?.(next)
setOpen(false)
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
disabled={disabled}
data-slot="date-picker-trigger"
data-empty={!date}
className={cn(
"w-56 justify-start gap-2 font-normal",
!date && "text-muted-foreground",
className
)}
{...props}
>
<CalendarIcon className="size-4" />
<span className="truncate">
{date ? format(date, formatStr, { locale }) : placeholder}
</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
locale={locale}
{...calendarProps}
selected={date}
onSelect={select}
autoFocus
/>
</PopoverContent>
</Popover>
)
}
export { DatePicker, type DatePickerProps }Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the colors will be missing.
Usage
import { DatePicker } from "@/components/ui/date-picker"
<DatePicker placeholder="Select a date" />Control it with value and onValueChange, or drop it in with an optional defaultValue and let it manage its own selection.
Examples
Uncontrolled default value
Pass defaultValue for an initial date and let the component track the selection on its own, no state wiring required.
Placeholder
Disabled
Custom width
The trigger defaults to 224px wide. Pass a width utility through className to match a form column.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | Date | undefined | The controlled selected date. Pair it with onValueChange to drive the picker from your own state. |
defaultValue | Date | undefined | The initial date for uncontrolled use. The component then tracks selection internally. |
onValueChange | (date: Date | undefined) => void | undefined | Fires with the newly selected date, or undefined when the selection is cleared. The popover closes after a pick. |
placeholder | string | "Pick a date" | Muted text shown on the trigger when no date is selected. |
locale | Locale | undefined | A date-fns locale object. It localizes the trigger label and is forwarded to the inner Calendar, so weekday and month names follow it too. |
formatStr | string | "PPP" | The date-fns format pattern used for the trigger label when a date is selected. |
calendarProps | ComponentProps<typeof Calendar> | undefined | Extra props forwarded to the inner Calendar, such as disabled matchers, startMonth or captionLayout. mode, selected and onSelect stay managed by the picker. |
disabled | boolean | false | Disable the trigger so the popover can no longer be opened. |
className | string | undefined | Classes for the trigger Button. The default is a 224px wide, left-aligned field; override the width or other styles here. |
ai2 Date Picker: a calendar-in-popover date field for React
The ai2 Date Picker is a shadcn-compatible date field for React that opens the ai2 Calendar inside a Popover. Click the trigger, pick a day, and the selected date shows on the button formatted with date-fns. It works controlled through value and onValueChange, or uncontrolled through defaultValue.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Date Picker?
It composes three ai2 components: an outline Button trigger with a lucide calendar icon, a Popover for the floating panel, and the ai2 Calendar in single mode for the grid. The whole field is exported as one DatePicker component.
It keeps a small internal state so it works with no wiring at all, while still accepting value and onValueChange for full control. The trigger formats the date with the date-fns PPP pattern, for example July 9th, 2026, and shows the placeholder in muted text when nothing is selected. Selecting a day closes the popover automatically.
Why use it
- Controlled or uncontrolled: Drive it from state with value and onValueChange, or drop it in with an optional defaultValue and let it manage its own selection.
- One import, three parts wired: The Button trigger, Popover and Calendar are already composed and connected, so you get a complete date field from a single component.
- Readable formatting: The selected date renders through the date-fns PPP pattern, a long, human-friendly format, with no manual toLocaleDateString wiring.
- Consistent with your controls: The trigger is an outline ai2 Button and the panel is the ai2 Calendar, so the field matches your inputs and buttons in both light and dark mode.
- Agent-readable metadata: The registry item describes its props and composition in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, date-fns, the ai2 Button, Calendar and Popover dependencies and the @ai2/tokens theme to your project.
- Popover-mounted calendar: The Calendar renders inside a Popover aligned to the trigger, so it floats above the layout and never gets clipped by overflow containers.
- Auto-close on select: Picking a day updates the value and closes the popover in one motion, matching how users expect a date field to behave.
- Placeholder and empty state: A data-empty attribute and muted text mark the unselected state, so the trigger reads as a prompt until a date is chosen.
- Width via className: The trigger defaults to a 224px left-aligned field; pass a className to widen it, match a form column or restyle it entirely.
- Disabled support: The disabled prop turns off the trigger so the popover cannot open, for read-only forms or closed booking windows.
Production tips
- Lift state when you need the value: For forms, control the picker with value and onValueChange so the selected Date lives in your state and can be validated or submitted.
- Match your form column width: The default trigger is 224px wide. Pass className="w-full" or a fixed width so the field lines up with the inputs around it.
- Reach for Calendar directly for ranges: The Date Picker is single-date only. For a start and end range, use the ai2 Calendar in range mode inside your own Popover.
- Localize the format if needed: Pass a date-fns locale to render the trigger label and the calendar in another language, and use formatStr to change the date-fns pattern away from the PPP default.
- Set a helpful placeholder: Replace the default Pick a date with the field's purpose, for example Choose a check-in day, so the empty state guides the user.
Works with the rest of ai2
The Date Picker is built from the ai2 Calendar, the ai2 Popover and an outline ai2 Button, so it inherits their focus rings, animations and token styling for free.
Pair it with a ai2 Label and ai2 Field to build a labelled form row, and reach for the Calendar on its own when you need multiple or range selection. Everything shares one token source, so combinations stay visually consistent in both modes.