Vela UI Docsvenizia.ai

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-picker
npx shadcn@latest add @velaui/date-picker

Examples

  • Default
  • Date range
  • Date and time
  • Centered
  • Disabled dates

View code

Date Picker · Default

Installation

pnpm dlx velaui add @velaui/date-picker
pnpm dlx velaui add @velaui/date-picker

Code

date-picker-default.tsx
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Label } from "@/components/ui/label";
import {
  DatePicker,
  DatePickerContent,
  DatePickerFooter,
  DatePickerTrigger,
} from "@/components/ui/date-picker";

const format = (date: Date) =>
  date.toLocaleDateString("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
  });

export default function Example() {
  const [open, setOpen] = React.useState(false);
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 5, 12),
  );
  // Staged until Apply — the field only changes when the user commits.
  const [draft, setDraft] = React.useState(selected);

  return (
    <div className="flex w-[280px] flex-col gap-2">
      <Label htmlFor="date-picker-single">Single date</Label>
      <DatePicker
        open={open}
        onOpenChange={(next) => {
          if (next) setDraft(selected);
          setOpen(next);
        }}
      >
        <DatePickerTrigger id="date-picker-single">
          {selected ? format(selected) : undefined}
        </DatePickerTrigger>
        <DatePickerContent>
          <Calendar
            mode="single"
            selected={draft}
            onSelect={setDraft}
            defaultMonth={draft ?? new Date(2024, 5)}
          />
          <DatePickerFooter>
            <Button variant="outline" onClick={() => setOpen(false)}>
              Cancel
            </Button>
            <Button
              variant="primary"
              onClick={() => {
                setSelected(draft);
                setOpen(false);
              }}
            >
              Apply
            </Button>
          </DatePickerFooter>
        </DatePickerContent>
      </DatePicker>
    </div>
  );
}

View code

Date Picker · Date range

Installation

pnpm dlx velaui add @velaui/date-picker
pnpm dlx velaui add @velaui/date-picker

Code

date-picker-date-range.tsx
import * as React from "react";
import type { DateRange } from "react-day-picker";
import { Button } from "@/components/ui/button";
import { CalendarRange } from "@/components/ui/calendar";
import { Label } from "@/components/ui/label";
import {
  DatePicker,
  DatePickerContent,
  DatePickerFooter,
  DatePickerTrigger,
} from "@/components/ui/date-picker";

const format = (date: Date) =>
  date.toLocaleDateString("en-US", { month: "short", day: "numeric" });

const formatRange = (range: DateRange | undefined) => {
  if (!range?.from) return undefined;
  return range.to
    ? `${format(range.from)} – ${format(range.to)}`
    : format(range.from);
};

export default function Example() {
  const [open, setOpen] = React.useState(false);
  const [range, setRange] = React.useState<DateRange | undefined>({
    from: new Date(2024, 5, 10),
    to: new Date(2024, 5, 18),
  });
  const [draft, setDraft] = React.useState(range);

  return (
    <div className="flex w-[280px] flex-col gap-2">
      <Label htmlFor="date-picker-range">Date range</Label>
      <DatePicker
        open={open}
        onOpenChange={(next) => {
          if (next) setDraft(range);
          setOpen(next);
        }}
      >
        <DatePickerTrigger id="date-picker-range" placeholder="Select dates">
          {formatRange(range)}
        </DatePickerTrigger>
        <DatePickerContent>
          {/* Two panels, each with its own month — CalendarRange keeps the pair
              from crossing over. */}
          <CalendarRange
            selected={draft}
            onSelect={setDraft}
            defaultMonth={draft?.from ?? new Date(2024, 5)}
          />
          <DatePickerFooter>
            <Button variant="outline" onClick={() => setOpen(false)}>
              Cancel
            </Button>
            <Button
              variant="primary"
              onClick={() => {
                setRange(draft);
                setOpen(false);
              }}
            >
              Apply
            </Button>
          </DatePickerFooter>
        </DatePickerContent>
      </DatePicker>
    </div>
  );
}

View code

Date Picker · Date and time

Installation

pnpm dlx velaui add @velaui/date-picker
pnpm dlx velaui add @velaui/date-picker

Code

date-picker-date-and-time.tsx
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Label } from "@/components/ui/label";
import {
  DatePicker,
  DatePickerContent,
  DatePickerFooter,
  DatePickerSection,
  DatePickerTrigger,
} from "@/components/ui/date-picker";

const TIME_SLOTS = ["09:00", "10:30", "13:00", "15:30"];

const format = (date: Date) =>
  date.toLocaleDateString("en-US", { month: "short", day: "numeric" });

export default function Example() {
  const [open, setOpen] = React.useState(false);
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 5, 12),
  );
  const [time, setTime] = React.useState(TIME_SLOTS[1]);

  return (
    <div className="flex w-[280px] flex-col gap-2">
      <Label htmlFor="date-picker-datetime">Date & time</Label>
      <DatePicker open={open} onOpenChange={setOpen}>
        <DatePickerTrigger
          id="date-picker-datetime"
          placeholder="Select date & time"
        >
          {selected ? `${format(selected)} at ${time}` : undefined}
        </DatePickerTrigger>
        <DatePickerContent>
          <Calendar
            mode="single"
            selected={selected}
            onSelect={setSelected}
            defaultMonth={selected ?? new Date(2024, 5)}
          />
          <DatePickerSection>
            <span className="text-sm font-medium text-text-strong">Time</span>
            <div className="flex flex-wrap gap-2">
              {TIME_SLOTS.map((slot) => (
                <Button
                  key={slot}
                  variant={slot === time ? "primary" : "outline"}
                  size="xs"
                  onClick={() => setTime(slot)}
                >
                  {slot}
                </Button>
              ))}
            </div>
          </DatePickerSection>
          <DatePickerFooter>
            <Button variant="outline" onClick={() => setOpen(false)}>
              Cancel
            </Button>
            <Button variant="primary" onClick={() => setOpen(false)}>
              Apply
            </Button>
          </DatePickerFooter>
        </DatePickerContent>
      </DatePicker>
    </div>
  );
}

View code

Date Picker · Centered

Installation

pnpm dlx velaui add @velaui/date-picker
pnpm dlx velaui add @velaui/date-picker

Code

date-picker-centered.tsx
import * as React from "react";
import { Calendar } from "@/components/ui/calendar";
import { Label } from "@/components/ui/label";
import {
  DatePicker,
  DatePickerContent,
  DatePickerTrigger,
} from "@/components/ui/date-picker";

const format = (date: Date) =>
  date.toLocaleDateString("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
  });

export default function Example() {
  const [open, setOpen] = React.useState(false);
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 5, 20),
  );

  // No footer: picking a day is the commit, so the panel closes on select.
  return (
    <div className="flex w-[280px] flex-col gap-2">
      <Label htmlFor="date-picker-centered">Centered header</Label>
      <DatePicker open={open} onOpenChange={setOpen}>
        <DatePickerTrigger id="date-picker-centered">
          {selected ? format(selected) : undefined}
        </DatePickerTrigger>
        <DatePickerContent>
          <Calendar
            mode="single"
            headerVariant="center"
            selected={selected}
            onSelect={(date) => {
              setSelected(date);
              setOpen(false);
            }}
            defaultMonth={selected ?? new Date(2024, 5)}
          />
        </DatePickerContent>
      </DatePicker>
    </div>
  );
}

View code

Date Picker · Disabled dates

Installation

pnpm dlx velaui add @velaui/date-picker
pnpm dlx velaui add @velaui/date-picker

Code

date-picker-disabled-dates.tsx
import * as React from "react";
import { Calendar } from "@/components/ui/calendar";
import { Label } from "@/components/ui/label";
import {
  DatePicker,
  DatePickerContent,
  DatePickerTrigger,
} from "@/components/ui/date-picker";

const format = (date: Date) =>
  date.toLocaleDateString("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
  });

export default function Example() {
  const [open, setOpen] = React.useState(false);
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 5, 12),
  );

  return (
    <div className="flex w-[280px] flex-col gap-2">
      <Label htmlFor="date-picker-disabled">Weekdays only</Label>
      <DatePicker open={open} onOpenChange={setOpen}>
        <DatePickerTrigger id="date-picker-disabled">
          {selected ? format(selected) : undefined}
        </DatePickerTrigger>
        <DatePickerContent>
          <Calendar
            mode="single"
            selected={selected}
            onSelect={(date) => {
              setSelected(date);
              setOpen(false);
            }}
            defaultMonth={selected ?? new Date(2024, 5)}
            disabled={{ dayOfWeek: [0, 6] }}
          />
        </DatePickerContent>
      </DatePicker>
    </div>
  );
}

Usage

import * as React from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Label } from "@/components/ui/label";
import {
  DatePicker,
  DatePickerContent,
  DatePickerFooter,
  DatePickerTrigger,
} from "@/components/ui/date-picker";

const format = (date: Date) =>
  date.toLocaleDateString("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
  });

export default function Example() {
  const [open, setOpen] = React.useState(false);
  const [selected, setSelected] = React.useState<Date | undefined>(
    new Date(2024, 5, 12),
  );
  // Staged until Apply — the field only changes when the user commits.
  const [draft, setDraft] = React.useState(selected);

  return (
    <div className="flex w-[280px] flex-col gap-2">
      <Label htmlFor="date-picker-single">Single date</Label>
      <DatePicker
        open={open}
        onOpenChange={(next) => {
          if (next) setDraft(selected);
          setOpen(next);
        }}
      >
        <DatePickerTrigger id="date-picker-single">
          {selected ? format(selected) : undefined}
        </DatePickerTrigger>
        <DatePickerContent>
          <Calendar
            mode="single"
            selected={draft}
            onSelect={setDraft}
            defaultMonth={draft ?? new Date(2024, 5)}
          />
          <DatePickerFooter>
            <Button variant="outline" onClick={() => setOpen(false)}>
              Cancel
            </Button>
            <Button
              variant="primary"
              onClick={() => {
                setSelected(draft);
                setOpen(false);
              }}
            >
              Apply
            </Button>
          </DatePickerFooter>
        </DatePickerContent>
      </DatePicker>
    </div>
  );
}

Props

DatePicker

Takes every prop of DatePickerProps.

DatePickerContent

PropTypeDefault
align"center" | "start" | "end""start"
sideOffsetnumber8

Takes these and every prop of DatePickerContentProps.

DatePickerFooter

Takes every prop of DatePickerFooterProps.

DatePickerSection

Takes every prop of DatePickerSectionProps.

DatePickerTrigger

PropTypeDefault
iconstring | 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 />
placeholderstring"Select date"

Takes these and every prop of DatePickerTriggerProps.

DatePicker

Takes every prop of DatePickerProps.

DatePickerContent

PropTypeDefault
alignimport("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").Align"start"
sideOffsetnumber | import("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").OffsetFunction8

Takes these and every prop of DatePickerContentProps.

DatePickerFooter

Takes every prop of DatePickerFooterProps.

DatePickerSection

Takes every prop of DatePickerSectionProps.

DatePickerTrigger

PropTypeDefault
iconstring | 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 />
placeholderstring"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,
};