Date Picker
A calendar in a popover for picking a single date, a date range, or a date with time.
npx shadcn@latest add @velaui/date-pickerpnpm dlx velaui add @velaui/date-pickeryarn dlx shadcn@latest add @velaui/date-pickerbunx --bun shadcn@latest add @velaui/date-pickerveni add date-pickerInstalls the copy of vela-ui that veni-builder serves to Venizia apps; veni-builder serves the Radix UI copy only, so this tab exists for that base alone.
npx shadcn@latest add @velaui/date-pickerpnpm dlx velaui add @velaui/date-pickeryarn dlx shadcn@latest add @velaui/date-pickerbunx --bun shadcn@latest add @velaui/date-pickerExamples
- Default
- Date range
- Date and time
- Centered
- Disabled dates
Usage
Props
DatePicker
Takes every prop of DatePickerProps.
DatePickerContent
| Prop | Type | Default |
|---|---|---|
align | "center" | "start" | "end" | "start" |
sideOffset | number | 8 |
Takes these and every prop of DatePickerContentProps.
DatePickerFooter
Takes every prop of DatePickerFooterProps.
DatePickerSection
Takes every prop of DatePickerSectionProps.
DatePickerTrigger
| Prop | Type | Default |
|---|---|---|
icon | string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | <Calendar01 aria-hidden /> |
placeholder | string | "Select date" |
Takes these and every prop of DatePickerTriggerProps.
DatePicker
Takes every prop of DatePickerProps.
DatePickerContent
| Prop | Type | Default |
|---|---|---|
align | import("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").Align | "start" |
sideOffset | number | import("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").OffsetFunction | 8 |
Takes these and every prop of DatePickerContentProps.
DatePickerFooter
Takes every prop of DatePickerFooterProps.
DatePickerSection
Takes every prop of DatePickerSectionProps.
DatePickerTrigger
| Prop | Type | Default |
|---|---|---|
icon | string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | <Calendar01 aria-hidden /> |
placeholder | string | "Select date" |
Takes these and every prop of DatePickerTriggerProps.
Dependencies
- npm
radix-ui@1.6.0- Installs with it
- 1 icon
- npm
@base-ui/react@1.6.0- Installs with it
- 1 icon
Source
components/ui/date-picker.tsx
import * as React from "react";
import { Calendar01 } from "@/components/icons/calendar-01";
import { Popover as PopoverPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
/* -------------------------------------------------------------------------- */
/* Date Picker */
/* -------------------------------------------------------------------------- */
export type DatePickerProps = React.ComponentProps<typeof PopoverPrimitive.Root>;
/**
* The date picker is a composition, not a component: a trigger field, a floating
* panel, and whichever calendar the case needs. Figma draws the panel itself
* ("Date Picker": Single date · Dual dates · Date & time) as Calendar plus a
* bottom panel, and captions the trigger as "VelaUI Input + calendar-01 icon" —
* so the calendar lives in `calendar.tsx` and is imported here rather than
* copied, and the trigger borrows the input's own field styling.
*
* ```tsx
* <DatePicker>
* <DatePickerTrigger>{label}</DatePickerTrigger>
* <DatePickerContent>
* <Calendar mode="single" selected={date} onSelect={setDate} />
* <DatePickerFooter>…</DatePickerFooter>
* </DatePickerContent>
* </DatePicker>
* ```
*
* The calendar brings its own px-6 py-5, so the panel adds no padding of its
* own — only the footer and any extra section are laid out here.
*/
function DatePicker({
...props
}: DatePickerProps) {
return <PopoverPrimitive.Root data-slot="date-picker" {...props} />;
}
export type DatePickerTriggerProps = Omit<React.ComponentProps<"button">, "type"> & {
placeholder?: string;
icon?: React.ReactNode;
};
/**
* The field that opens the panel. It is a button rather than an input — the
* value is picked, never typed — so it reproduces the outlined input's box
* instead of reusing `Input`, which would put a text caret on a control that
* cannot take one.
*/
function DatePickerTrigger({
className,
children,
placeholder = "Select date",
icon = <Calendar01 aria-hidden />,
...props
}: DatePickerTriggerProps) {
return (
<PopoverPrimitive.Trigger asChild>
<button
type="button"
data-slot="date-picker-trigger"
data-empty={children ? undefined : true}
className={cn(
"flex h-10 w-full min-w-0 items-center gap-2 rounded-md border border-border-default bg-bg-main px-3 py-2.5 text-left text-sm leading-[1.4] text-text-strong transition-colors outline-none select-none",
"hover:border-border-hover hover:bg-input-hover",
"focus-visible:border-ring-neutral-outline focus-visible:bg-input-default focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow",
"aria-expanded:border-ring-neutral-outline",
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:border-border-disabled disabled:bg-input-disabled disabled:text-text-disabled",
// With nothing picked the field reads as a placeholder, matching what
// a real input would show.
"data-empty:text-text-note",
"[&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-icon-strong",
className,
)}
{...props}
>
{icon}
<span className="min-w-0 flex-1 truncate">
{children ?? placeholder}
</span>
</button>
</PopoverPrimitive.Trigger>
);
}
export type DatePickerContentProps = React.ComponentProps<typeof PopoverPrimitive.Content>;
/**
* The floating panel. Padding deliberately lives on the sections inside: the
* footer is full-bleed against the surface with only a rule above it, so the
* panel has to clip rather than pad.
*/
function DatePickerContent({
className,
align = "start",
sideOffset = 8,
...props
}: DatePickerContentProps) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="date-picker-content"
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-fit origin-(--radix-popover-content-transform-origin) overflow-hidden rounded-xl border border-border-default bg-bg-main text-text-strong shadow-sm outline-none",
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
export type DatePickerSectionProps = React.ComponentProps<"div">;
/**
* A block below the calendar — the time list of the "Date & time" composition.
* Ruled off from the calendar above it.
*/
function DatePickerSection({
className,
...props
}: DatePickerSectionProps) {
return (
<div
data-slot="date-picker-section"
className={cn(
"flex flex-col gap-2 border-t border-border-default px-6 py-4",
className,
)}
{...props}
/>
);
}
export type DatePickerFooterProps = React.ComponentProps<"div">;
/** Actions row — Figma splits the width evenly between the buttons. */
function DatePickerFooter({
className,
...props
}: DatePickerFooterProps) {
return (
<div
data-slot="date-picker-footer"
className={cn(
"flex items-center gap-3 border-t border-border-default p-4 [&>*]:flex-1",
className,
)}
{...props}
/>
);
}
export {
DatePicker,
DatePickerContent,
DatePickerFooter,
DatePickerSection,
DatePickerTrigger,
};components/ui/date-picker.tsx
import * as React from "react";
import { Calendar01 } from "@/components/icons/calendar-01";
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import { cn } from "@/lib/utils";
/* -------------------------------------------------------------------------- */
/* Date Picker */
/* -------------------------------------------------------------------------- */
export type DatePickerProps = React.ComponentProps<typeof PopoverPrimitive.Root>;
/**
* The date picker is a composition, not a component: a trigger field, a floating
* panel, and whichever calendar the case needs. Figma draws the panel itself
* ("Date Picker": Single date · Dual dates · Date & time) as Calendar plus a
* bottom panel, and captions the trigger as "VelaUI Input + calendar-01 icon" —
* so the calendar lives in `calendar.tsx` and is imported here rather than
* copied, and the trigger borrows the input's own field styling.
*
* ```tsx
* <DatePicker>
* <DatePickerTrigger>{label}</DatePickerTrigger>
* <DatePickerContent>
* <Calendar mode="single" selected={date} onSelect={setDate} />
* <DatePickerFooter>…</DatePickerFooter>
* </DatePickerContent>
* </DatePicker>
* ```
*
* The calendar brings its own px-6 py-5, so the panel adds no padding of its
* own — only the footer and any extra section are laid out here.
*/
function DatePicker({
...props
}: DatePickerProps) {
return <PopoverPrimitive.Root data-slot="date-picker" {...props} />;
}
export type DatePickerTriggerProps = Omit<React.ComponentProps<typeof PopoverPrimitive.Trigger>, "type"> & {
placeholder?: string;
icon?: React.ReactNode;
};
/**
* The field that opens the panel. It is a button rather than an input — the
* value is picked, never typed — so it reproduces the outlined input's box
* instead of reusing `Input`, which would put a text caret on a control that
* cannot take one.
*/
function DatePickerTrigger({
className,
children,
placeholder = "Select date",
icon = <Calendar01 aria-hidden />,
...props
}: DatePickerTriggerProps) {
return (
<PopoverPrimitive.Trigger
data-slot="date-picker-trigger"
data-empty={children ? undefined : true}
className={cn(
"flex h-10 w-full min-w-0 items-center gap-2 rounded-md border border-border-default bg-bg-main px-3 py-2.5 text-left text-sm leading-[1.4] text-text-strong transition-colors outline-none select-none",
"hover:border-border-hover hover:bg-input-hover",
"focus-visible:border-ring-neutral-outline focus-visible:bg-input-default focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow",
"aria-expanded:border-ring-neutral-outline",
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:border-border-disabled disabled:bg-input-disabled disabled:text-text-disabled",
// With nothing picked the field reads as a placeholder, matching what
// a real input would show.
"data-empty:text-text-note",
"[&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-icon-strong",
className,
)}
{...props}
>
{icon}
<span className="min-w-0 flex-1 truncate">
{children ?? placeholder}
</span>
</PopoverPrimitive.Trigger>
);
}
export type DatePickerContentProps = React.ComponentProps<typeof PopoverPrimitive.Popup> &
Pick<
React.ComponentProps<typeof PopoverPrimitive.Positioner>,
"align" | "alignOffset" | "side" | "sideOffset"
>;
/**
* The floating panel. Padding deliberately lives on the sections inside: the
* footer is full-bleed against the surface with only a rule above it, so the
* panel has to clip rather than pad.
*/
function DatePickerContent({
className,
align = "start",
sideOffset = 8,
alignOffset,
side,
...props
}: DatePickerContentProps) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<PopoverPrimitive.Popup
data-slot="date-picker-content"
className={cn(
"z-50 w-fit origin-(--transform-origin) overflow-hidden rounded-xl border border-border-default bg-bg-main text-text-strong shadow-sm outline-none",
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
/>
</PopoverPrimitive.Positioner>
</PopoverPrimitive.Portal>
);
}
export type DatePickerSectionProps = React.ComponentProps<"div">;
/**
* A block below the calendar — the time list of the "Date & time" composition.
* Ruled off from the calendar above it.
*/
function DatePickerSection({
className,
...props
}: DatePickerSectionProps) {
return (
<div
data-slot="date-picker-section"
className={cn(
"flex flex-col gap-2 border-t border-border-default px-6 py-4",
className,
)}
{...props}
/>
);
}
export type DatePickerFooterProps = React.ComponentProps<"div">;
/** Actions row — Figma splits the width evenly between the buttons. */
function DatePickerFooter({
className,
...props
}: DatePickerFooterProps) {
return (
<div
data-slot="date-picker-footer"
className={cn(
"flex items-center gap-3 border-t border-border-default p-4 [&>*]:flex-1",
className,
)}
{...props}
/>
);
}
export {
DatePicker,
DatePickerContent,
DatePickerFooter,
DatePickerSection,
DatePickerTrigger,
};