Forms
DatePicker
DatePicker pairs a built-in popover trigger with a built-in calendar grid, supporting single or range selection, min/max bounds, and specific disabled dates.
Installation
npx shadcn add https://rck.vibeboxph.com//r/date-picker.jsonLive preview & controls
Controls
<DatePicker />Anatomy
date-picker-triggerThe button showing the formatted date or range.date-picker-contentThe popover panel containing the calendar.DayPickerThe built-in calendar grid.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| mode | "single" | "range" | single | Selection mode. |
| value / defaultValue | Date | null | DateRange | null | — | Controlled / uncontrolled selection (shape depends on `mode`). |
| onValueChange | (value) => void | — | Fires on selection change. |
| minDate / maxDate | Date | — | Inclusive bounds; dates outside are disabled. |
| disabledDates | Date[] | — | Specific dates to disable, e.g. holidays. |
| numberOfMonths | number | 1 (2 for range) | Months rendered side by side. |
| clearable | boolean | true | Shows a Clear action once a value is set. |
| dateFormat | string | PPP | Intl.DateTimeFormat options or preset for the trigger label. |
Accessibility
- The trigger is a real button with `aria-haspopup="dialog"` and `aria-expanded`.
- The calendar grid is keyboard-navigable (Arrow keys move between days, Enter selects, Escape closes) via the built-in Calendar primitive.
- Disabled days are marked `aria-disabled` and excluded from tab order.
Source
registry/rck/ui/date-picker.tsx
"use client";
import * as React from "react";
import {
DayPicker,
type DateRange,
type Matcher,
formatDate,
} from "@/registry/rck/primitives/calendar";
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from "@/lib/icons";
import { cn } from "@/lib/utils";
import { PopoverRoot, PopoverContent, PopoverTrigger } from "@/registry/rck/ui/popover";
export type { DateRange };
const dayPickerClassNames = {
root: "p-3",
months: "flex flex-col gap-4 sm:flex-row",
month: "flex flex-col gap-3",
month_caption: "flex items-center justify-center pt-1 relative",
caption_label: "text-sm font-medium",
nav: "flex items-center justify-between absolute inset-x-1 top-1",
button_previous: cn(
"inline-flex size-7 items-center justify-center rounded-md border border-border-strong",
"text-muted-foreground hover:bg-surface-2 hover:text-foreground disabled:opacity-40",
),
button_next: cn(
"inline-flex size-7 items-center justify-center rounded-md border border-border-strong",
"text-muted-foreground hover:bg-surface-2 hover:text-foreground disabled:opacity-40",
),
month_grid: "w-full border-collapse mt-2",
weekdays: "flex",
weekday: "text-muted-foreground w-9 text-[0.8rem] font-normal text-center",
weeks: "flex flex-col gap-1 mt-1",
week: "flex w-full",
day: "size-9 p-0 text-center text-sm relative",
day_button: cn(
"size-9 rounded-md text-sm font-normal inline-flex items-center justify-center",
"hover:bg-surface-2 aria-selected:opacity-100 outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
),
selected:
"[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary/90",
today: "[&>button]:border [&>button]:border-primary/60 [&>button]:font-semibold",
outside: "text-muted-foreground/50",
disabled: "text-muted-foreground/30 pointer-events-none",
range_start:
"[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:rounded-l-md [&>button]:rounded-r-none",
range_middle:
"bg-primary-soft [&>button]:bg-transparent [&>button]:text-primary-soft-foreground [&>button]:rounded-none",
range_end:
"[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:rounded-r-md [&>button]:rounded-l-none",
hidden: "invisible",
};
export interface DatePickerBaseProps extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
"value" | "defaultValue" | "onChange"
> {
placeholder?: string;
dateFormat?: string;
minDate?: Date;
maxDate?: Date;
disabledDates?: Date[];
numberOfMonths?: number;
clearable?: boolean;
contentClassName?: string;
/** Fires when the calendar popover opens or closes. */
onOpenChange?: (open: boolean) => void;
}
export interface SingleDatePickerProps extends DatePickerBaseProps {
mode?: "single";
value?: Date | null;
defaultValue?: Date | null;
onValueChange?: (value: Date | null) => void;
}
export interface RangeDatePickerProps extends DatePickerBaseProps {
mode: "range";
value?: DateRange | null;
defaultValue?: DateRange | null;
onValueChange?: (value: DateRange | null) => void;
}
export type DatePickerProps = SingleDatePickerProps | RangeDatePickerProps;
function buildDisabledMatcher({
minDate,
maxDate,
disabledDates,
}: Pick<DatePickerBaseProps, "minDate" | "maxDate" | "disabledDates">):
Matcher | Matcher[] | undefined {
const matchers: Matcher[] = [];
if (minDate) matchers.push({ before: minDate });
if (maxDate) matchers.push({ after: maxDate });
if (disabledDates?.length) matchers.push(disabledDates);
return matchers.length ? matchers : undefined;
}
export function DatePicker(props: DatePickerProps) {
const {
placeholder = "Pick a date",
dateFormat = "PPP",
minDate,
maxDate,
disabledDates,
numberOfMonths,
disabled,
clearable = true,
className,
contentClassName,
id,
value,
defaultValue,
onValueChange,
onOpenChange,
mode: rawMode,
onBlur,
onFocus,
onKeyDown,
...triggerProps
} = props;
const mode = rawMode ?? "single";
const resolvedNumberOfMonths = numberOfMonths ?? (mode === "range" ? 2 : 1);
const [open, setOpen] = React.useState(false);
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = React.useState<
Date | DateRange | null | undefined
>(defaultValue ?? null);
const currentValue = isControlled ? value : internalValue;
const disabledMatcher = buildDisabledMatcher({ minDate, maxDate, disabledDates });
function commit(next: Date | DateRange | null) {
if (!isControlled) setInternalValue(next);
(onValueChange as ((value: Date | DateRange | null) => void) | undefined)?.(next);
}
const label = React.useMemo(() => {
if (!currentValue) return placeholder;
if (mode === "range") {
const range = currentValue as DateRange;
if (range.from && range.to) {
return `${formatDate(range.from, dateFormat)} – ${formatDate(range.to, dateFormat)}`;
}
if (range.from) return `${formatDate(range.from, dateFormat)} – …`;
return placeholder;
}
return formatDate(currentValue as Date, dateFormat);
}, [currentValue, dateFormat, mode, placeholder]);
return (
<PopoverRoot
open={open}
onOpenChange={(nextOpen) => {
setOpen(nextOpen);
onOpenChange?.(nextOpen);
}}
>
<PopoverTrigger asChild>
<button
type="button"
id={id}
disabled={disabled}
data-slot="date-picker-trigger"
onBlur={onBlur}
onFocus={onFocus}
onKeyDown={onKeyDown}
className={cn(
"border-border-strong bg-background flex h-[39px] w-full items-center gap-2 rounded-md border px-3 text-[11px] shadow-sm",
"focus-visible:border-primary focus-visible:ring-ring/30 transition-colors outline-none focus-visible:ring-2",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...triggerProps}
>
<CalendarIcon
className="text-muted-foreground size-4 shrink-0"
aria-hidden="true"
/>
<span className={cn("truncate", !currentValue && "text-muted-foreground")}>
{label}
</span>
</button>
</PopoverTrigger>
<PopoverContent
data-slot="date-picker-content"
align="start"
className={cn("w-auto p-0", contentClassName)}
>
{mode === "range" ? (
<DayPicker
mode="range"
numberOfMonths={resolvedNumberOfMonths}
selected={(currentValue as DateRange | undefined) ?? undefined}
onSelect={(range) => commit(range ?? null)}
disabled={disabledMatcher}
classNames={dayPickerClassNames}
components={{
Chevron: ({ orientation }) =>
orientation === "left" ? (
<ChevronLeft className="size-4" />
) : (
<ChevronRight className="size-4" />
),
}}
/>
) : (
<DayPicker
mode="single"
numberOfMonths={resolvedNumberOfMonths}
selected={(currentValue as Date | undefined) ?? undefined}
onSelect={(date) => {
commit(date ?? null);
setOpen(false);
}}
disabled={disabledMatcher}
classNames={dayPickerClassNames}
components={{
Chevron: ({ orientation }) =>
orientation === "left" ? (
<ChevronLeft className="size-4" />
) : (
<ChevronRight className="size-4" />
),
}}
/>
)}
{clearable && currentValue && (
<div className="border-border border-t p-2">
<button
type="button"
onClick={() => commit(null)}
className="text-muted-foreground hover:bg-surface-2 hover:text-foreground w-full rounded-md px-2 py-1.5 text-xs"
>
Clear
</button>
</div>
)}
</PopoverContent>
</PopoverRoot>
);
}
DatePicker.displayName = "DatePicker";
registry/rck/ui/popover.tsx
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@/registry/rck/primitives/popover";
import { X } from "@/lib/icons";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/registry/rck/ui/button";
/** Attribute placed on external trigger elements. Pair with the Popover `trigger` prop. */
export const POPUP_TRIGGER_ATTR = "data-popup-id";
export const POPUP_TRIGGER_ALIAS_ATTR = "data-popup";
/** @deprecated Use `Popover` for the common case, or import primitives directly. */
export const PopoverRoot = PopoverPrimitive.Root;
export const PopoverAnchor = PopoverPrimitive.Anchor;
export const PopoverClose = PopoverPrimitive.Close;
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverContent = React.forwardRef<
React.ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 12, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"group border-border bg-popover text-popover-foreground z-50 w-72 overflow-visible rounded-lg border p-4 shadow-md outline-none",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
"data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
>
<PopoverPrimitive.Arrow
aria-hidden="true"
className={cn(
"fill-popover stroke-border absolute stroke-[1.5]",
"group-data-[side=bottom]:-top-[6px] group-data-[side=bottom]:left-1/2 group-data-[side=bottom]:-translate-x-1/2 group-data-[side=bottom]:rotate-180",
"group-data-[side=top]:-bottom-[6px] group-data-[side=top]:left-1/2 group-data-[side=top]:-translate-x-1/2",
"group-data-[side=right]:top-1/2 group-data-[side=right]:-left-[9px] group-data-[side=right]:-translate-y-1/2 group-data-[side=right]:rotate-90",
"group-data-[side=left]:top-1/2 group-data-[side=left]:-right-[9px] group-data-[side=left]:-translate-y-1/2 group-data-[side=left]:-rotate-90",
)}
/>
{props.children}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
function usePopupTriggerElement(trigger: string | undefined, attribute: string) {
const [element, setElement] = React.useState<HTMLElement | null>(null);
React.useLayoutEffect(() => {
if (!trigger) return;
const selector = `[${attribute}="${CSS.escape(trigger)}"]`;
function resolveTrigger() {
setElement(document.querySelector<HTMLElement>(selector));
}
resolveTrigger();
const observer = new MutationObserver(resolveTrigger);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [attribute],
});
return () => observer.disconnect();
}, [trigger, attribute]);
return trigger ? element : null;
}
export interface PopoverProps extends Omit<
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>,
"content" | "children" | "title"
> {
content: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
/** Inline trigger passed as a child element. */
children?: React.ReactElement;
/** Matches `${POPUP_TRIGGER_ATTR}` on an external trigger element. */
trigger?: string;
/** Matches `data-popup` on an external trigger element. Use when the trigger is outside this component. */
popupTrigger?: string;
/** With `trigger`, skip auto click toggle and control open state manually. */
manualTrigger?: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
modal?: boolean;
contentClassName?: string;
showClose?: boolean;
className?: string;
}
function PopoverBody({
title,
description,
content,
showClose,
}: Pick<PopoverProps, "title" | "description" | "content" | "showClose">) {
return (
<>
{(title || description || showClose) && (
<div className="mb-3 flex items-start justify-between gap-3">
<div className="space-y-1">
{title ? <p className="text-sm font-medium">{title}</p> : null}
{description ? (
<p className="text-muted-foreground text-xs">{description}</p>
) : null}
</div>
{showClose ? (
<PopoverClose
aria-label="Close"
className={cn(
"text-muted-foreground rounded-sm outline-none",
"hover:text-foreground focus-visible:ring-ring/50 focus-visible:ring-2",
)}
>
<X className="size-4" />
</PopoverClose>
) : null}
</div>
)}
<div>{content}</div>
</>
);
}
export function Popover({
content,
title,
description,
children,
trigger,
popupTrigger,
manualTrigger = false,
open,
defaultOpen,
onOpenChange,
modal,
className,
contentClassName,
showClose = false,
placement,
align,
sideOffset,
alignOffset,
...contentProps
}: PopoverProps) {
if (trigger && popupTrigger) {
throw new Error("Popover accepts either `trigger` or `popupTrigger`, not both.");
}
const externalTrigger = trigger ?? popupTrigger;
const triggerElement = usePopupTriggerElement(
externalTrigger,
popupTrigger ? POPUP_TRIGGER_ALIAS_ATTR : POPUP_TRIGGER_ATTR,
);
const hasInlineTrigger = Boolean(children);
if (!hasInlineTrigger && !externalTrigger) {
throw new Error("Popover requires `children`, `trigger`, or `popupTrigger`.");
}
if (hasInlineTrigger && externalTrigger) {
throw new Error(
"Popover accepts either `children` or an external trigger, not both.",
);
}
return (
<PopoverRoot
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
modal={modal}
manualTrigger={manualTrigger}
triggerElement={hasInlineTrigger ? undefined : triggerElement}
>
{children ? <PopoverTrigger asChild>{children}</PopoverTrigger> : null}
<PopoverContent
placement={placement}
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn(contentClassName, className)}
{...contentProps}
>
<PopoverBody
title={title}
description={description}
content={content}
showClose={showClose}
/>
</PopoverContent>
</PopoverRoot>
);
}
/** Default styles for a standalone popover trigger button (optional helper). */
export const popoverTriggerButtonClassName = buttonVariants({
variant: "outline",
size: "md",
});
Related examples
Single datePick a single date.
Date rangePick a start and end date across two months.
Disabled datesBounded range with specific dates disabled.