---
title: "Date Picker"
description: "Single-date and date-range selection with shortcuts, manual confirmation, localized calendars and editable date fields."
---

Interactive preview: https://ui.coderocket.app/docs/components/date-picker

## Usage

Get this component from a [saved library export](/docs/export) or the [connected CLI](/docs/agents). The imports below refer to local files installed in your app.

Import `styles/components.css`, then `styles/theme.css` once. Wrap your app in `ThemeScope` from `components/ui/utils` so typography, focus and overlays share the theme. The [installation guide](/docs/export) covers runtime dependencies and React/Next.js setup.

```tsx
import { DatePicker } from './components/ui/date-picker';

<DatePicker mode="range" label="Project dates" defaultValue={{start:"2026-09-17",end:"2026-09-23"}} autoApply={false} onValueChange={range=>console.info(range ? range.start + " – " + range.end : "Cleared")}/>
```

## Single dates and ranges

Single mode preserves the original CodeRocket API: `value` is a `YYYY-MM-DD` string or `null`; clearing calls `onValueChange("")`. Range mode uses `{ start: "YYYY-MM-DD", end: "YYYY-MM-DD" }` or `null`. These are calendar dates, not timestamps. Store them directly without converting through local time or `toISOString()`.

```tsx
<DatePicker label="Due date" name="dueDate" defaultValue="2026-10-05" />

<DatePicker
  mode="range"
  label="Travel dates"
  name="travel"
  autoApply={false}
  defaultValue={{ start: "2026-10-05", end: "2026-10-12" }}
/>
```

`autoApply={false}` keeps a draft until Apply. Cancel, Escape or dismissing the popover leaves the committed value unchanged. Range form fields use `travel[start]` and `travel[end]`. Clearing a manually confirmed picker is also a draft change until Apply.

## Availability rules

Use `min` / `max` for inclusive boundaries, and `disabledDates` for individual unavailable dates or a predicate receiving an ISO date. By default a range cannot cross an unavailable date; `excludeDisabled={false}` permits unavailable interior dates while keeping both endpoints available.

```tsx
<DatePicker
  mode="range"
  label="Booking dates"
  min="2026-10-01"
  max="2026-12-31"
  disabledDates={["2026-10-15", "2026-10-16"]}
  startFrom="2026-10-01"
/>
```

Validate dates again when processing a booking on your server. The picker handles selection rules; it does not reserve inventory or calculate time slots.

## Shortcuts and inline display

Built-in shortcuts include Today and Yesterday; range mode also provides the last 7/30 days, this month and last month. Set `shortcuts={false}` to hide them or supply custom values. Unavailable shortcuts are disabled.

```tsx
<DatePicker
  mode="range"
  inline
  numberOfMonths={2}
  shortcuts={[
    { label: "Launch week", value: { start: "2026-10-05", end: "2026-10-09" } },
  ]}
  showWeekNumbers
  weekStartsOn={1}
/>
```

## Localization and presentation

`locale` formats months, weekdays and selected dates. French and English action labels are built in. Override `labels` for another language. `weekStartsOn` accepts 0 (Sunday) through 6 (Saturday). `weekdayFormat` adjusts weekday labels, and `dateFormat` or `formatDate` customizes the trigger text without changing the value.

```tsx
<DatePicker
  mode="range"
  label="Dates du projet"
  locale="fr-FR"
  weekStartsOn={1}
  autoApply={false}
  dateFormat={{ day: "numeric", month: "long", year: "numeric" }}
  separator=" → "
/>
```

Native date fields in the panel allow direct entry and follow the browser's language. Set `showInputs={false}` for calendar-only selection. Use `triggerClassName`, `icon`, `renderTrigger` and `renderDay` for custom presentation; `renderTrigger` provides the content inside the existing accessible button, so do not return another button. `overlay` adds a modal backdrop. Colors and dark appearance follow your ThemeScope tokens.

## Coming from Vue Tailwind Datepicker

The default React component now covers the legacy picker’s principal selection features: single/range, two months, shortcuts, unavailable dates, Apply/Cancel, clearing, inline display, month/year selection and localization. React uses typed ISO values and callbacks instead of Vue `v-model`, Day.js formatter tokens and slots. It is not a drop-in Vue replacement, and there is no implicit conversion of the old array/string/object models.

## Design system

This component consumes your semantic colors, spacing, typography and shape tokens. Import the compiled styles and your theme once as described in [Export](/docs/export).

Icons use Lucide React. Install `lucide-react@1.47.0` alongside React and Base UI.

Dependencies: [Calendar](/docs/components/calendar), [Button](/docs/components/button).

## Behavior and accessibility

- Single mode returns an ISO date; range mode returns `{ start, end }`. Display formatting never changes stored values.
- Choose one or two months, navigate by keyboard or month/year controls, and enter dates directly.
- Shortcuts, disabled dates, min/max bounds, week numbers, inline display, clearing and Apply/Cancel are available.
- French and English control labels are built in; other languages can supply labels alongside an Intl locale.
- The same source and compiled styles are included in the library export.

Status: **experimental** during early access. Sample previews do not send emails, process payments or upload files to a service.

## Source and props

This is the exported source, using the same implementation as the editor.

```tsx
"use client";
import {
  useRef,
  useState,
  useEffect,
  useEffectEvent,
  type ReactNode,
} from "react";
import { Popover as Base } from "@base-ui/react/popover";
import { CalendarDays, X } from "lucide-react";
import {
  Calendar,
  parseCalendarDate,
  calendarLabels,
  useCalendarToday,
  addCalendarDays,
  addCalendarMonths,
  startOfCalendarMonth,
  endOfCalendarMonth,
  isCalendarDateDisabled,
  isCalendarRangeAvailable,
  type CalendarBaseProps,
  type DateRange,
} from "./calendar";
import { Button } from "./button";
import { cx, usePortalContainer } from "./utils";

type PickerValue = string | DateRange | null;
export type DatePickerShortcut = {
  label: string;
  value: string | DateRange | (() => string | DateRange);
};
export type DatePickerProps = CalendarBaseProps & {
  name?: string;
  form?: string;
  placeholder?: string;
  separator?: string;
  autoApply?: boolean;
  inline?: boolean;
  overlay?: boolean;
  clearable?: boolean;
  showInputs?: boolean;
  shortcuts?: boolean | readonly DatePickerShortcut[];
  formatDate?: (date: string, locale: string) => string;
  dateFormat?: Intl.DateTimeFormatOptions;
  triggerClassName?: string;
  icon?: ReactNode;
  renderTrigger?: (value: string, placeholder: string) => ReactNode;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
} & (
    | {
        mode?: "single";
        value?: string | null;
        defaultValue?: string;
        onValueChange?: (date: string) => void;
      }
    | {
        mode: "range";
        value?: DateRange | null;
        defaultValue?: DateRange;
        onValueChange?: (range: DateRange | null) => void;
      }
  );

export function DatePicker(props: DatePickerProps) {
  const {
    locale = "en",
    disabled = false,
    autoApply = true,
    inline = false,
    overlay = false,
    clearable = true,
    showInputs = true,
    shortcuts = true,
    separator = " – ",
    name,
    className,
    triggerClassName,
  } = props;
  const labels = calendarLabels(locale, props.labels),
    label =
      props.label ??
      (props.mode === "range" ? labels.chooseRange : labels.chooseDate);
  const placeholder = props.placeholder ?? label,
    container = usePortalContainer();
  const root = useRef<HTMLDivElement>(null);
  const [internal, setInternal] = useState<PickerValue>(
      props.defaultValue ?? null,
    ),
    [internalOpen, setInternalOpen] = useState(false);
  const chosen: PickerValue =
    props.value !== undefined ? props.value : internal;
  const [selectionPending, setSelectionPending] = useState(false);
  const [calendarKey, setCalendarKey] = useState(0);
  const [draft, setDraft] = useState<PickerValue>(chosen),
    [error, setError] = useState("");
  const getStart = (value: PickerValue) =>
    typeof value === "string" ? value : (value?.start ?? "");
  const getEnd = (value: PickerValue) =>
    typeof value === "object" && value ? value.end : "";
  const [startInput, setStartInput] = useState(getStart(chosen)),
    [endInput, setEndInput] = useState(getEnd(chosen));
  const synchronize = useEffectEvent(() => {
    setDraft(chosen);
    setStartInput(getStart(chosen));
    setEndInput(getEnd(chosen));
    setError("");
  });
  const chosenStart = getStart(chosen),
    chosenEnd = getEnd(chosen);
  useEffect(() => {
    synchronize();
  }, [chosenStart, chosenEnd]);
  const resetForm = useEffectEvent(() => {
    const next =
      props.value === undefined ? (props.defaultValue ?? null) : props.value;
    if (props.value === undefined) setInternal(next);
    setDraft(next);
    setStartInput(getStart(next));
    setEndInput(getEnd(next));
    setSelectionPending(false);
    setCalendarKey((current) => current + 1);
    setError("");
    if (props.open === undefined) setInternalOpen(false);
    props.onOpenChange?.(false);
  });
  useEffect(() => {
    const form = props.form
      ? document.getElementById(props.form)
      : root.current?.closest("form");
    if (!(form instanceof HTMLFormElement)) return;
    const reset = (event: Event) => {
      queueMicrotask(() => {
        if (!event.defaultPrevented) resetForm();
      });
    };
    form.addEventListener("reset", reset);
    return () => form.removeEventListener("reset", reset);
  }, [props.form]);
  const open = (props.open ?? internalOpen) && !disabled;
  function setOpen(next: boolean) {
    if (next && disabled) return;
    if (props.open === undefined) setInternalOpen(next);
    if (next) {
      setDraft(chosen);
      setStartInput(getStart(chosen));
      setEndInput(getEnd(chosen));
      setError("");
    }
    props.onOpenChange?.(next);
  }
  const options = {
    min: props.min,
    max: props.max,
    disabledDates: props.disabledDates,
    excludeDisabled: props.excludeDisabled,
  };
  function valid(value: PickerValue) {
    if (!value) return true;
    if (typeof value === "object")
      return props.mode === "range" && isCalendarRangeAvailable(value, options);
    try {
      parseCalendarDate(value);
    } catch {
      return false;
    }
    return props.mode !== "range" && !isCalendarDateDisabled(value, options);
  }
  function commit(value: PickerValue) {
    if (disabled || !valid(value)) return;
    if (props.value === undefined) setInternal(value);
    if (props.mode === "range")
      props.onValueChange?.(value && typeof value === "object" ? value : null);
    else props.onValueChange?.(typeof value === "string" ? value : "");
  }
  function select(value: PickerValue, close = true, resetCalendar = false) {
    if (disabled) return;
    if (!valid(value)) {
      setError(labels.invalidDate);
      return;
    }
    const visibleValue =
      autoApply && props.value !== undefined ? chosen : value;
    setDraft(visibleValue);
    setSelectionPending(false);
    if (resetCalendar) setCalendarKey((current) => current + 1);
    setStartInput(getStart(visibleValue));
    setEndInput(getEnd(visibleValue));
    setError("");
    if (autoApply) {
      commit(value);
      if (!inline && close) setOpen(false);
    }
  }
  function edit(start: string, end: string) {
    setStartInput(start);
    setEndInput(end);
    const next =
      props.mode === "range" ? (start && end ? { start, end } : null) : start;
    if ((props.mode === "range" && !!start !== !!end) || !valid(next)) {
      setError(labels.invalidDate);
      return;
    }
    setError("");
    setDraft(next);
    setSelectionPending(false);
    setCalendarKey((current) => current + 1);
    if (autoApply) commit(next);
  }
  function finishInput() {
    if (autoApply && props.value !== undefined && !error) {
      setStartInput(getStart(chosen));
      setEndInput(getEnd(chosen));
    }
  }
  const format = (date: string) =>
    props.formatDate?.(date, locale) ??
    new Intl.DateTimeFormat(
      locale,
      props.dateFormat
        ? { ...props.dateFormat, timeZone: "UTC", calendar: "gregory" }
        : { dateStyle: "medium", timeZone: "UTC", calendar: "gregory" },
    ).format(parseCalendarDate(date));
  const display = chosen
    ? typeof chosen === "string"
      ? format(chosen)
      : `${format(chosen.start)}${separator}${format(chosen.end)}`
    : "";
  const localToday = useCalendarToday();
  const today = localToday || "1970-01-01",
    yesterday = addCalendarDays(today, -1),
    thisMonth = startOfCalendarMonth(today),
    lastMonth = startOfCalendarMonth(addCalendarMonths(today, -1));
  const defaults: DatePickerShortcut[] =
    props.mode === "range"
      ? [
          { label: labels.today, value: { start: today, end: today } },
          {
            label: labels.yesterday,
            value: { start: yesterday, end: yesterday },
          },
          {
            label: labels.last7Days,
            value: { start: addCalendarDays(today, -6), end: today },
          },
          {
            label: labels.last30Days,
            value: { start: addCalendarDays(today, -29), end: today },
          },
          {
            label: labels.thisMonth,
            value: { start: thisMonth, end: endOfCalendarMonth(today) },
          },
          {
            label: labels.lastMonth,
            value: { start: lastMonth, end: endOfCalendarMonth(lastMonth) },
          },
        ]
      : [
          { label: labels.today, value: today },
          { label: labels.yesterday, value: yesterday },
        ];
  const items = Array.isArray(shortcuts)
    ? shortcuts
    : shortcuts
      ? defaults
      : [];
  const common: CalendarBaseProps = {
    label,
    min: props.min,
    max: props.max,
    locale,
    weekStartsOn: props.weekStartsOn,
    disabled,
    disabledDates: props.disabledDates,
    excludeDisabled: props.excludeDisabled,
    startFrom: props.startFrom,
    numberOfMonths: props.numberOfMonths,
    showWeekNumbers: props.showWeekNumbers,
    weekdayFormat: props.weekdayFormat,
    monthSelection: props.monthSelection,
    labels: props.labels,
    previousMonthLabel: props.previousMonthLabel,
    nextMonthLabel: props.nextMonthLabel,
    renderDay: props.renderDay,
    onMonthChange: props.onMonthChange,
    onSelectionPendingChange: (pending) => {
      setSelectionPending(pending);
      props.onSelectionPendingChange?.(pending);
    },
  };
  const fields =
    name &&
    (props.mode === "range" ? (
      <>
        <input
          type="hidden"
          form={props.form}
          name={`${name}[start]`}
          value={getStart(chosen)}
          disabled={disabled}
        />
        <input
          type="hidden"
          form={props.form}
          name={`${name}[end]`}
          value={getEnd(chosen)}
          disabled={disabled}
        />
      </>
    ) : (
      <input
        type="hidden"
        form={props.form}
        name={name}
        value={getStart(chosen)}
        disabled={disabled}
      />
    ));
  const calendarValue = autoApply ? chosen : draft;
  const content = (
    <>
      <div className="cr-date-picker-body">
        {items.length > 0 && (
          <div className="cr-date-picker-shortcuts" aria-label={label}>
            {items.map((item, index) => {
              const value =
                typeof item.value === "function" ? item.value() : item.value;
              return (
                <Button
                  key={index}
                  variant="ghost"
                  size="sm"
                  disabled={
                    disabled ||
                    (!localToday && !Array.isArray(shortcuts)) ||
                    !valid(value)
                  }
                  onClick={() => select(value, true, true)}
                >
                  {item.label}
                </Button>
              );
            })}
          </div>
        )}
        {showInputs && (
          <div className="cr-date-picker-inputs">
            <label>
              <span>{props.mode === "range" ? labels.startDate : label}</span>
              <input
                type="date"
                onBlur={finishInput}
                value={startInput}
                min={props.min ?? "0001-01-01"}
                max={props.max ?? "9999-12-31"}
                disabled={disabled}
                aria-invalid={!!error || undefined}
                onChange={(event) => edit(event.target.value, endInput)}
              />
            </label>
            {props.mode === "range" && (
              <label>
                <span>{labels.endDate}</span>
                <input
                  type="date"
                  onBlur={finishInput}
                  value={endInput}
                  min={startInput || props.min || "0001-01-01"}
                  max={props.max ?? "9999-12-31"}
                  disabled={disabled}
                  aria-invalid={!!error || undefined}
                  onChange={(event) => edit(startInput, event.target.value)}
                />
              </label>
            )}
          </div>
        )}
        <div>
          {props.mode === "range" ? (
            <Calendar
              {...common}
              key={calendarKey}
              mode="range"
              value={
                calendarValue && typeof calendarValue === "object"
                  ? calendarValue
                  : null
              }
              onValueChange={select}
            />
          ) : (
            <Calendar
              {...common}
              key={calendarKey}
              value={typeof calendarValue === "string" ? calendarValue : null}
              onValueChange={select}
            />
          )}
        </div>
        {error && (
          <p className="cr-date-picker-error" role="alert">
            {error}
          </p>
        )}
      </div>
      {(clearable || !autoApply) && (
        <div className="cr-date-picker-footer">
          {clearable && (
            <Button
              variant="ghost"
              size="sm"
              disabled={disabled || (!startInput && !endInput)}
              onClick={() =>
                select(props.mode === "range" ? null : "", false, true)
              }
            >
              {labels.clear}
            </Button>
          )}
          {!autoApply && (
            <div className="cr-date-picker-actions">
              <Button
                variant="ghost"
                size="sm"
                disabled={disabled}
                onClick={() => {
                  setDraft(chosen);
                  setSelectionPending(false);
                  setCalendarKey((current) => current + 1);
                  setStartInput(getStart(chosen));
                  setEndInput(getEnd(chosen));
                  setError("");
                  if (!inline) setOpen(false);
                }}
              >
                {labels.cancel}
              </Button>
              <Button
                size="sm"
                disabled={
                  disabled || selectionPending || !!error || !valid(draft)
                }
                onClick={() => {
                  commit(draft);
                  if (!inline) setOpen(false);
                }}
              >
                {labels.apply}
              </Button>
            </div>
          )}
        </div>
      )}
    </>
  );
  if (inline)
    return (
      <div
        ref={root}
        className={cx("cr-date-picker cr-date-picker-inline", className)}
        role="group"
        aria-label={label}
      >
        {fields}
        {content}
      </div>
    );
  return (
    <div ref={root} className={cx("cr-date-picker", className)}>
      {fields}
      <Base.Root
        open={open}
        onOpenChange={setOpen}
        modal={overlay ? true : "trap-focus"}
      >
        <Base.Trigger
          className={cx("cr-button cr-date-picker-trigger", triggerClassName)}
          data-variant="outline"
          disabled={disabled}
          aria-label={`${label}${display ? `: ${display}` : ""}`}
        >
          {props.renderTrigger ? (
            props.renderTrigger(display, placeholder)
          ) : (
            <>
              <span data-placeholder={!display || undefined}>
                {display || placeholder}
              </span>
              {props.icon ?? <CalendarDays size={16} aria-hidden="true" />}
            </>
          )}
        </Base.Trigger>
        <Base.Portal container={container}>
          {overlay && <Base.Backdrop className="cr-date-picker-backdrop" />}
          <Base.Positioner
            sideOffset={8}
            collisionPadding={16}
            collisionAvoidance={{ side: "shift", align: "shift" }}
            className="cr-positioner cr-date-picker-positioner"
          >
            <Base.Popup
              className="cr-popup cr-date-picker-popup"
              // Focus the first control (Close), keeping month controls visible on opening.
              initialFocus={true}
            >
              <div className="cr-date-picker-heading">
                <Base.Title>{label}</Base.Title>
                <Base.Close
                  className="cr-date-picker-close"
                  aria-label={labels.close}
                >
                  <X size={16} aria-hidden="true" />
                </Base.Close>
              </div>
              {content}
            </Base.Popup>
          </Base.Positioner>
        </Base.Portal>
      </Base.Root>
    </div>
  );
}
```
