# Date Picker

> A form input that opens Calendar in a popover, adding presets, time selection, and month or year quick-nav.

A shadcn/ui block from the [@wensity](https://ui.wensity.com/components) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/wensity/date-picker).

- Page, with a live preview: https://shadcn.exchange/r/wensity/date-picker
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/wensity/date-picker-light-82e75af022fb4741.png
- Registry item: https://raw.githubusercontent.com/wensity/registry/main/date-picker.json
- Categories: UI Primitives, wensity, free
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://raw.githubusercontent.com/wensity/registry/main/date-picker.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@base-ui/react`, `@tabler/icons-react`, `clsx`, `framer-motion`, `tailwind-merge`
- Registry items: `@wensity/calendar`

## Notes from the registry

Free Wensity component. Installs to @components/wensity/date-picker.tsx. For Pro components and updates, use pnpm dlx wensity@latest add date-picker.

## Files

### `registry/wensity/lib/utils.ts`

```ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

### `registry/wensity/date-picker.tsx`

```tsx
"use client";

import * as React from "react";
import { IconCalendar, IconClock, IconX } from "@tabler/icons-react";
import { Popover as BasePopover } from "@base-ui/react/popover";
import { cn } from "@/lib/utils";
import { Calendar, type DateRange } from "@/components/wensity/calendar";

export type { DateRange };

/* ─── Types ─────────────────────────────────────────────────── */

export interface DatePreset {
  label: string;
  getValue: () => Date | DateRange | Date[];
}

export type DatePickerMode = "single" | "range" | "multiple";

interface DatePickerBaseProps {
  /** Selection mode. */
  mode?: DatePickerMode;
  /** Placeholder text in the trigger when no date is selected. */
  placeholder?: string;
  /** Disabled state. */
  disabled?: boolean;
  /** Return true to disable specific dates. */
  dateDisabled?: (date: Date) => boolean;
  /** 0 = Sunday, 1 = Monday. Default 0. */
  weekStartsOn?: 0 | 1;
  /** Earliest selectable date. */
  fromDate?: Date;
  /** Latest selectable date. */
  toDate?: Date;

  /* ── Variant toggles ── */

  /** Show preset quick-selection buttons. Pass an array to customize or `true` for defaults. Default: `true`. */
  presets?: boolean | DatePreset[];
  /** Show time picker (hours + minutes) alongside the calendar. */
  dateTime?: boolean;
  /** Enable month and year quick-nav pickers. Default: `true`. */
  monthYear?: boolean;

  /** Additional class name for the root wrapper. */
  className?: string;
}

interface DatePickerSingleProps extends DatePickerBaseProps {
  mode?: "single";
  /** Controlled value. */
  value?: Date;
  /** Uncontrolled default value. */
  defaultValue?: Date;
  /** Called when selection changes. */
  onChange?: (value: Date | undefined) => void;
}

interface DatePickerRangeProps extends DatePickerBaseProps {
  mode: "range";
  /** Controlled value. */
  value?: DateRange;
  /** Uncontrolled default value. */
  defaultValue?: DateRange;
  /** Called when selection changes. */
  onChange?: (value: DateRange | undefined) => void;
}

interface DatePickerMultipleProps extends DatePickerBaseProps {
  mode: "multiple";
  /** Controlled value. */
  value?: Date[];
  /** Uncontrolled default value. */
  defaultValue?: Date[];
  /** Called when selection changes. */
  onChange?: (value: Date[]) => void;
}

export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps | DatePickerMultipleProps;

/* ─── Constants ─────────────────────────────────────────────── */

const TRIGGER_SURFACE = cn(
  "inline-flex h-[var(--primitive-control-height-md)] min-h-[var(--primitive-control-height-md)] items-center gap-2",
  "rounded-[var(--primitive-radius-control)] border border-[var(--border)] px-3 text-sm",
  "bg-[var(--background)] text-[var(--foreground)]",
  "transition-[border-color,background-color,box-shadow] duration-150 ease-[var(--primitive-ease,cubic-bezier(0.23,1,0.32,1))]",
  "hover:border-[color-mix(in_srgb,var(--foreground)_14%,transparent)]",
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--primitive-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)]",
  "disabled:pointer-events-none disabled:opacity-40",
  "data-[open]:border-[color-mix(in_srgb,var(--foreground)_24%,transparent)]",
);

const POPOVER_CONTENT_CLASS = cn(
  "rounded-[var(--primitive-radius-surface)] border border-[var(--border)] p-3",
  "bg-[var(--primitive-surface-overlay)] text-[var(--foreground)]",
  "[box-shadow:var(--primitive-shadow-overlay)]",
  "z-[var(--primitive-z-popover)] origin-[var(--transform-origin)] transform-gpu outline-none",
  "transition-[opacity,scale] duration-[180ms] ease-[var(--primitive-ease,cubic-bezier(0.23,1,0.32,1))]",
  "data-[ending-style]:duration-[120ms]",
  "data-[starting-style]:opacity-0 data-[ending-style]:opacity-0",
  "data-[starting-style]:scale-[0.96] data-[ending-style]:scale-[0.96]",
  "motion-reduce:transition-none motion-reduce:data-[starting-style]:scale-100 motion-reduce:data-[ending-style]:scale-100",
);

const PRESET_ITEM_CLASS = cn(
  "rounded-[var(--primitive-radius-item)] px-2 py-1.5 text-left text-xs font-medium",
  "text-[var(--muted-foreground)] transition-colors duration-150",
  "hover:bg-[var(--primitive-surface-selected)] hover:text-[var(--foreground)]",
  "focus-visible:outline-none focus-visible:bg-[var(--primitive-surface-selected)] focus-visible:text-[var(--foreground)]",
);

const TIME_FIELD_CLASS = cn(
  "rounded-[var(--primitive-radius-control)] border border-[var(--border)] bg-transparent px-1.5 py-0.5",
  "text-[var(--foreground)] outline-none",
  "focus-visible:ring-2 focus-visible:ring-[var(--primitive-ring)]",
);

/* ─── Date Helpers ──────────────────────────────────────────── */

function startOfDay(d: Date): Date {
  return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}

function isBefore(a: Date, b: Date): boolean {
  return startOfDay(a).getTime() < startOfDay(b).getTime();
}

/* ─── Formatting ────────────────────────────────────────────── */

function formatDate(d: Date): string {
  const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
}

function formatShortDate(d: Date): string {
  const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  return `${months[d.getMonth()]} ${d.getDate()}`;
}

function formatDateRange(range: DateRange): string {
  if (range.from && range.to) {
    return `${formatShortDate(range.from)} – ${formatShortDate(range.to)}`;
  }
  if (range.from) return formatDate(range.from);
  return "";
}

function formatMultipleDates(dates: Date[]): string {
  if (dates.length === 0) return "";
  if (dates.length === 1) return formatDate(dates[0]);
  return `${dates.length} dates selected`;
}

/* ─── Presets ────────────────────────────────────────────────── */

function buildDefaultPresets(mode: DatePickerMode): DatePreset[] {
  const today = startOfDay(new Date());
  const yesterday = new Date(today);
  yesterday.setDate(yesterday.getDate() - 1);

  if (mode === "multiple") {
    return [
      { label: "Today", getValue: () => [today] },
      { label: "Tomorrow", getValue: () => { const d = new Date(today); d.setDate(d.getDate() + 1); return [d]; } },
      { label: "Yesterday", getValue: () => [yesterday] },
    ];
  }

  if (mode === "single") {
    return [
      { label: "Today", getValue: () => today },
      { label: "Tomorrow", getValue: () => { const d = new Date(today); d.setDate(d.getDate() + 1); return d; } },
      { label: "Yesterday", getValue: () => yesterday },
    ];
  }

  return [
    {
      label: "Today",
      getValue: () => ({ from: today, to: today }),
    },
    {
      label: "Last 7 days",
      getValue: () => {
        const from = new Date(today);
        from.setDate(from.getDate() - 6);
        return { from, to: today };
      },
    },
    {
      label: "Last 30 days",
      getValue: () => {
        const from = new Date(today);
        from.setDate(from.getDate() - 29);
        return { from, to: today };
      },
    },
    {
      label: "This month",
      getValue: () => {
        const from = new Date(today.getFullYear(), today.getMonth(), 1);
        const to = new Date(today.getFullYear(), today.getMonth() + 1, 0);
        return { from, to };
      },
    },
  ];
}

/* ─── Time Picker Sub-component ─────────────────────────────── */

function TimePicker({
  value,
  onChange,
}: {
  value: Date;
  onChange: (date: Date) => void;
}) {
  const hours = value.getHours();
  const minutes = value.getMinutes();

  return (
    <div data-wensity-primitive="" className="flex items-center gap-1.5 border-t border-[var(--border)] pt-2.5">
      <IconClock size={14} stroke={1.75} className="shrink-0 text-[var(--muted-foreground)]" />
      <div className="flex items-center gap-1 text-xs">
        <select
          value={hours}
          onChange={(e) => {
            const d = new Date(value);
            d.setHours(Number(e.target.value));
            onChange(d);
          }}
          className={TIME_FIELD_CLASS}
        >
          {Array.from({ length: 24 }, (_, i) => (
            <option key={i} value={i}>
              {String(i).padStart(2, "0")}
            </option>
          ))}
        </select>
        <span className="text-[var(--muted-foreground)]">:</span>
        <select
          value={minutes}
          onChange={(e) => {
            const d = new Date(value);
            d.setMinutes(Number(e.target.value));
            onChange(d);
          }}
          className={TIME_FIELD_CLASS}
        >
          {Array.from({ length: 60 }, (_, i) => (
            <option key={i} value={i}>
              {String(i).padStart(2, "0")}
            </option>
          ))}
        </select>
      </div>
    </div>
  );
}

/* ─── DatePicker ────────────────────────────────────────────── */

function DatePickerInner(props: DatePickerProps) {
  const {
    mode = "single",
    placeholder = "Pick a date",
    disabled: isDisabled = false,
    dateDisabled,
    weekStartsOn = 0,
    fromDate,
    toDate,
    presets: presetsProp = true,
    dateTime = false,
    monthYear = true,
    className,
  } = props as DatePickerBaseProps;

  const {
    value: controlledValue,
    defaultValue,
    onChange,
  } = props as {
    value?: Date | DateRange | Date[];
    defaultValue?: Date | DateRange | Date[];
    onChange?: (value: Date | DateRange | Date[] | undefined) => void;
  };

  const [open, setOpen] = React.useState(false);

  const isControlled = "value" in props;

  const [internalSingle, setInternalSingle] = React.useState<Date | undefined>(
    mode === "single" ? (defaultValue as Date | undefined) : undefined,
  );
  const [internalRange, setInternalRange] = React.useState<DateRange | undefined>(
    mode === "range" ? (defaultValue as DateRange | undefined) : undefined,
  );
  const [internalMultiple, setInternalMultiple] = React.useState<Date[]>(
    mode === "multiple" ? ((defaultValue as Date[] | undefined) ?? []) : [],
  );

  const currentSingle = mode === "single"
    ? ((isControlled ? controlledValue : internalSingle) as Date | undefined)
    : undefined;
  const currentRange = mode === "range"
    ? ((isControlled ? controlledValue : internalRange) as DateRange | undefined)
    : undefined;
  const currentMultiple = mode === "multiple"
    ? (Array.isArray(isControlled ? controlledValue : internalMultiple)
      ? ((isControlled ? controlledValue : internalMultiple) as Date[])
      : [])
    : [];

  const [displayMonth, setDisplayMonth] = React.useState(() => {
    if (mode === "single" && currentSingle) return startOfDay(currentSingle);
    if (mode === "range" && currentRange?.from) return startOfDay(currentRange.from);
    if (mode === "multiple" && currentMultiple.length > 0) return startOfDay(currentMultiple[0]);
    if (fromDate) return startOfDay(fromDate);
    return startOfDay(new Date());
  });

  // Follow the controlled value to its month. This was already a manual
  // previous-value comparison inside an effect; doing it during render is the
  // same logic without rendering the old month first.
  const [controlledSync, setControlledSync] = React.useState({
    mode,
    value: controlledValue,
  });
  if (controlledSync.mode !== mode || controlledSync.value !== controlledValue) {
    setControlledSync({ mode, value: controlledValue });
    if (mode === "single" && controlledValue) {
      setDisplayMonth(startOfDay(controlledValue as Date));
    } else if (mode === "range" && (controlledValue as DateRange)?.from) {
      setDisplayMonth(startOfDay((controlledValue as DateRange).from!));
    } else if (mode === "multiple" && (controlledValue as Date[])?.length > 0) {
      setDisplayMonth(startOfDay((controlledValue as Date[])[0]));
    }
  }

  function commit(value: Date | DateRange | Date[] | undefined) {
    if (!isControlled) {
      if (mode === "single") setInternalSingle(value as Date | undefined);
      else if (mode === "range") setInternalRange(value as DateRange | undefined);
      else setInternalMultiple((value as Date[]) ?? []);
    }
    onChange?.(value);
  }

  function isDateDisabled(date: Date): boolean {
    if (fromDate && isBefore(date, startOfDay(fromDate))) return true;
    if (toDate && isBefore(startOfDay(toDate), date)) return true;
    return dateDisabled?.(date) ?? false;
  }

  function handleCalendarSelect(value: Date | Date[] | DateRange | undefined) {
    if (mode === "single") {
      let next = value as Date | undefined;
      if (next && dateTime) {
        const merged = new Date(next);
        merged.setHours(selectedTime.getHours(), selectedTime.getMinutes(), 0, 0);
        next = merged;
      }
      commit(next);
      if (next) setOpen(false);
      return;
    }

    if (mode === "multiple") {
      commit((value as Date[]) ?? []);
      return;
    }

    const next = value as DateRange | undefined;
    commit(next);
    if (next?.from && next?.to) setOpen(false);
  }

  function handlePreset(preset: DatePreset) {
    const val = preset.getValue();
    if (mode === "single") commit(val as Date);
    else if (mode === "range") commit(val as DateRange);
    else commit(Array.isArray(val) ? val : [val as Date]);
    setOpen(false);
  }

  function clearSelection() {
    commit(mode === "multiple" ? [] : undefined);
  }

  const showPresets = presetsProp !== false;
  const presets: DatePreset[] = React.useMemo(
    () => (Array.isArray(presetsProp) ? presetsProp : buildDefaultPresets(mode)),
    [presetsProp, mode],
  );

  const [selectedTime, setSelectedTime] = React.useState<Date>(() => {
    if (mode === "single" && currentSingle) return new Date(currentSingle);
    return new Date();
  });

  // Keep the time-of-day picker aligned with the selected date.
  const [timeSync, setTimeSync] = React.useState({
    dateTime,
    mode,
    value: currentSingle,
  });
  if (
    timeSync.dateTime !== dateTime ||
    timeSync.mode !== mode ||
    timeSync.value !== currentSingle
  ) {
    setTimeSync({ dateTime, mode, value: currentSingle });
    if (dateTime && mode === "single" && currentSingle) {
      setSelectedTime(new Date(currentSingle));
    }
  }

  const triggerLabel = React.useMemo(() => {
    if (mode === "single") {
      return currentSingle ? formatDate(currentSingle) : null;
    }
    if (mode === "range") {
      return currentRange?.from ? formatDateRange(currentRange) : null;
    }
    return currentMultiple.length > 0 ? formatMultipleDates(currentMultiple) : null;
  }, [mode, currentSingle, currentRange, currentMultiple]);

  const calendarSelected =
    mode === "single"
      ? currentSingle
      : mode === "range"
        ? currentRange
        : currentMultiple;

  return (
    <BasePopover.Root open={open} onOpenChange={setOpen}>
      <div className={cn("relative inline-flex items-center gap-1.5", className)}>
        <BasePopover.Trigger
          disabled={isDisabled}
          className={cn(TRIGGER_SURFACE, "min-w-[180px] justify-between")}
        >
          <span className={cn(!triggerLabel && "text-[var(--primitive-text-placeholder,var(--muted-foreground))]")}>
            {triggerLabel ?? placeholder}
          </span>
          <IconCalendar size={15} stroke={1.75} className="shrink-0 text-[var(--muted-foreground)]" />
        </BasePopover.Trigger>

        {triggerLabel ? (
          <button
            type="button"
            aria-label="Clear selection"
            disabled={isDisabled}
            onClick={clearSelection}
            className="flex size-5 shrink-0 items-center justify-center rounded-[var(--primitive-radius-control-sm)] text-[var(--muted-foreground)] transition-colors duration-150 hover:bg-[var(--primitive-surface-hover)] hover:text-[var(--foreground)] disabled:pointer-events-none disabled:opacity-40"
          >
            <IconX size={12} stroke={2} />
          </button>
        ) : null}
      </div>

      <BasePopover.Portal>
        <BasePopover.Positioner
          side="bottom"
          align="start"
          sideOffset={6}
          collisionPadding={12}
          className="z-[var(--primitive-z-popover)] outline-none"
        >
          <BasePopover.Popup data-wensity-primitive=""
            className={cn(POPOVER_CONTENT_CLASS, showPresets ? "w-[364px]" : "w-[288px]")}
          >
            <div className={cn("flex", showPresets && "gap-3")}>
              {showPresets ? (
                <div className="flex w-[100px] shrink-0 flex-col gap-0.5 border-r border-[var(--border)] pr-2">
                  {presets.map((preset) => (
                    <button
                      key={preset.label}
                      type="button"
                      onClick={() => handlePreset(preset)}
                      className={PRESET_ITEM_CLASS}
                    >
                      {preset.label}
                    </button>
                  ))}
                </div>
              ) : null}

              <div className="flex-1">
                {mode === "single" ? (
                  <Calendar
                    mode="single"
                    selected={calendarSelected as Date | undefined}
                    onSelect={handleCalendarSelect}
                    month={displayMonth}
                    onMonthChange={setDisplayMonth}
                    quickNav={monthYear}
                    weekStartsOn={weekStartsOn}
                    disabled={isDateDisabled}
                    className="max-w-none border-0 bg-transparent p-0"
                  />
                ) : mode === "range" ? (
                  <Calendar
                    mode="range"
                    selected={calendarSelected as DateRange | undefined}
                    onSelect={handleCalendarSelect}
                    month={displayMonth}
                    onMonthChange={setDisplayMonth}
                    quickNav={monthYear}
                    weekStartsOn={weekStartsOn}
                    disabled={isDateDisabled}
                    className="max-w-none border-0 bg-transparent p-0"
                  />
                ) : (
                  <Calendar
                    mode="multiple"
                    selected={calendarSelected as Date[]}
                    onSelect={handleCalendarSelect}
                    month={displayMonth}
                    onMonthChange={setDisplayMonth}
                    quickNav={monthYear}
                    weekStartsOn={weekStartsOn}
                    disabled={isDateDisabled}
                    className="max-w-none border-0 bg-transparent p-0"
                  />
                )}

                {dateTime ? (
                  <TimePicker
                    value={selectedTime}
                    onChange={(d) => {
                      setSelectedTime(d);
                      if (currentSingle) {
                        const merged = new Date(currentSingle);
                        merged.setHours(d.getHours(), d.getMinutes(), 0, 0);
                        commit(merged);
                      }
                    }}
                  />
                ) : null}
              </div>
            </div>
          </BasePopover.Popup>
        </BasePopover.Positioner>
      </BasePopover.Portal>
    </BasePopover.Root>
  );
}

/* ─── Export ────────────────────────────────────────────────── */

export const DatePicker = DatePickerInner as {
  (props: DatePickerSingleProps): React.ReactElement;
  (props: DatePickerRangeProps): React.ReactElement;
  (props: DatePickerMultipleProps): React.ReactElement;
};
```
