Date Picker
Single-date picker: a trigger that formats the date, opening shadcn Calendar in a popover. Takes react-day-picker matchers for bounds and disabled days.
Example
Installation
npx shadcn@latest add https://hirael.com/r/date-picker.jsonUsage
import { DatePicker, DatePickerTrigger, DatePickerContent } from "@/components/date-picker"Paths assume the default @/ alias from your components.json. The example above shows the parts composed.
API
<DatePicker />
| Prop | Type | Default |
|---|---|---|
value | Date | none |
defaultValue | Date | none |
onValueChange | ((value: Date | undefined) => void) | none |
open | boolean | none |
defaultOpen | boolean | false |
onOpenChange | ((open: boolean) => void) | none |
localeBCP 47 tag for the trigger label. Defaults to `en-US` so server and client render the same text. | string | 'en-US' |
disabled | boolean | none |
children | React.ReactNode | none |
<DatePickerTrigger />
+ native element props| Prop | Type | Default |
|---|---|---|
placeholder | string | 'Pick a date' |
children | React.ReactNode | none |
<DatePickerContent />
+ native element props| Prop | Type | Default |
|---|---|---|
buttonVariant | "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | none |
Component source
'use client';
import * as React from 'react';
import { CalendarIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/registry/hirael/bases/radix/ui/button';
import { Calendar } from '@/registry/hirael/bases/radix/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/registry/hirael/bases/radix/ui/popover';
import { useControllableState } from '@/registry/hirael/hooks/use-controllable-state';
interface DatePickerContextValue {
value: Date | undefined;
setValue: (value: Date | undefined) => void;
setOpen: (open: boolean) => void;
locale: string;
disabled?: boolean;
}
const DatePickerContext = React.createContext<DatePickerContextValue | null>(null);
const useDatePicker = () => {
const ctx = React.useContext(DatePickerContext);
if (!ctx) {
throw new Error('DatePicker compound parts must be used inside <DatePicker>');
}
return ctx;
};
export interface DatePickerProps {
value?: Date;
defaultValue?: Date;
onValueChange?: (value: Date | undefined) => void;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/** BCP 47 tag for the trigger label. Defaults to `en-US` so server and client render the same text. */
locale?: string;
disabled?: boolean;
children?: React.ReactNode;
}
const DatePicker = ({
value: valueProp,
defaultValue,
onValueChange,
open: openProp,
defaultOpen = false,
onOpenChange,
locale = 'en-US',
disabled,
children,
}: DatePickerProps) => {
const [value, setValue] = useControllableState({
prop: valueProp,
defaultProp: defaultValue,
onChange: onValueChange,
});
const [open, setOpen] = useControllableState({
prop: openProp,
defaultProp: defaultOpen,
onChange: onOpenChange,
});
const ctx = React.useMemo<DatePickerContextValue>(
() => ({ value, setValue, setOpen, locale, disabled }),
[value, setValue, setOpen, locale, disabled],
);
return (
<DatePickerContext.Provider value={ctx}>
<Popover open={open} onOpenChange={setOpen}>
{children}
</Popover>
</DatePickerContext.Provider>
);
};
interface DatePickerTriggerProps extends Omit<React.ComponentProps<'button'>, 'children'> {
placeholder?: string;
children?: React.ReactNode;
}
const DatePickerTrigger = ({ placeholder = 'Pick a date', className, children, ...props }: DatePickerTriggerProps) => {
const ctx = useDatePicker();
const fmt = React.useMemo(() => new Intl.DateTimeFormat(ctx.locale, { dateStyle: 'medium' }), [ctx.locale]);
return (
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
disabled={ctx.disabled}
data-slot="date-picker-trigger"
data-empty={!ctx.value || undefined}
className={cn('w-full justify-start font-normal tabular-nums data-empty:text-muted-foreground', className)}
{...props}
>
<CalendarIcon className="text-muted-foreground" />
<span data-slot="date-picker-trigger-label" className="flex-1 truncate text-start">
{children ?? (ctx.value ? fmt.format(ctx.value) : placeholder)}
</span>
</Button>
</PopoverTrigger>
);
};
type DatePickerContentProps = Omit<
React.ComponentProps<typeof Calendar>,
'mode' | 'selected' | 'onSelect' | 'required'
> &
Pick<React.ComponentProps<typeof PopoverContent>, 'align'>;
/** Takes the calendar's props (`disabled`, `startMonth`, `weekStartsOn`, …) and closes on pick. */
const DatePickerContent = ({ align = 'start', ...props }: DatePickerContentProps) => {
const ctx = useDatePicker();
return (
<PopoverContent align={align} data-slot="date-picker-content" className="w-auto p-0">
<Calendar
mode="single"
selected={ctx.value}
defaultMonth={ctx.value}
onSelect={(date) => {
ctx.setValue(date);
ctx.setOpen(false);
}}
{...props}
/>
</PopoverContent>
);
};
export { DatePicker, DatePickerTrigger, DatePickerContent };
Dependencies
shadcn registry
calendarpopoverbuttonuse-controllable-state
npm
lucide-react