f-ui
Components

Date Picker

Single-date picker with segmented input and popover calendar — locale-aware and built on React Aria.

DatePicker uses React Aria for the field, popover, and calendar. The value is a plain JS Date. Pass locale (BCP 47) to localize segments and the calendar; without it the component inherits from the nearest I18nProvider or the browser default.

Installing

pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/date-picker.json
npx shadcn@latest add https://ui.isaacfei.com/r/date-picker.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/r/date-picker.json
bun x shadcn@latest add https://ui.isaacfei.com/r/date-picker.json

Or with a namespace: npx shadcn@latest add @f-ui/date-picker.

The CLI installs npm dependencies (react-aria-components, @internationalized/date, lucide-react) and pulls registryDependencies: shared date-internals (calendar, date input, classes, value utils) plus shadcn primitives button, label, popover.

Usage

import { DatePicker } from '@/components/f-ui/date-picker/date-picker';

<DatePicker label="Date" value={date} onChange={setDate} />

Examples

Basic — single date, calendar trigger, optional helper copy under the field.

Date

Choose a day for your booking.

Selected:

"use client";

import { useState } from "react";

import { DatePicker } from "@/components/f-ui/date-picker/date-picker";

export function DatePickerDemo() {
  const [date, setDate] = useState<Date | undefined>(undefined);

  return (
    <div className="max-w-sm space-y-3">
      <DatePicker
        label="Date"
        value={date ?? null}
        onChange={setDate}
        placeholder="Choose a day for your booking."
      />
      <p className="text-muted-foreground text-xs">
        Selected:{" "}
        <span className="text-foreground font-medium tabular-nums">
          {date ? date.toLocaleDateString() : "—"}
        </span>
      </p>
    </div>
  );
}

Localized — switch locales to see month names, day headers, segment order, and first day of week.

Date

Locale: en-US

Selected:

"use client";

import { useState } from "react";

import { DatePicker } from "@/components/f-ui/date-picker/date-picker";

const locales = [
  { value: "en-US", label: "English (US)" },
  { value: "zh-CN", label: "中文 (简体)" },
  { value: "ja-JP", label: "日本語" },
  { value: "ar-SA", label: "العربية" },
  { value: "de-DE", label: "Deutsch" },
];

export function DatePickerI18nDemo() {
  const [date, setDate] = useState<Date | undefined>(undefined);
  const [locale, setLocale] = useState("en-US");

  return (
    <div className="max-w-sm space-y-4">
      <div className="flex flex-wrap gap-1.5">
        {locales.map((l) => (
          <button
            key={l.value}
            type="button"
            onClick={() => setLocale(l.value)}
            className={`rounded-md border px-2.5 py-1 text-xs transition-colors ${
              locale === l.value
                ? "border-primary bg-primary text-primary-foreground"
                : "border-border bg-background text-muted-foreground hover:bg-accent"
            }`}
          >
            {l.label}
          </button>
        ))}
      </div>
      <DatePicker
        locale={locale}
        label="Date"
        value={date ?? null}
        onChange={setDate}
        placeholder={`Locale: ${locale}`}
      />
      <p className="text-muted-foreground text-xs">
        Selected:{" "}
        <span className="text-foreground font-medium tabular-nums">
          {date ? date.toLocaleDateString(locale) : "—"}
        </span>
      </p>
    </div>
  );
}

Constrained — only weekdays within a window; weekends and out-of-range dates unavailable.

Appointment

Weekdays only, within the next 3 months.

Selected:

"use client";

import { useState } from "react";

import { DatePicker } from "@/components/f-ui/date-picker/date-picker";

const now = new Date();
const minDate = new Date(now.getFullYear(), now.getMonth(), 1);
const maxDate = new Date(now.getFullYear(), now.getMonth() + 3, 0);

function isWeekend(date: Date) {
  const day = date.getDay();
  return day === 0 || day === 6;
}

export function DatePickerConstrainedDemo() {
  const [date, setDate] = useState<Date | undefined>(undefined);

  return (
    <div className="max-w-sm space-y-3">
      <DatePicker
        label="Appointment"
        value={date ?? null}
        onChange={setDate}
        minValue={minDate}
        maxValue={maxDate}
        isDateUnavailable={isWeekend}
        placeholder="Weekdays only, within the next 3 months."
      />
      <p className="text-muted-foreground text-xs">
        Selected:{" "}
        <span className="text-foreground font-medium tabular-nums">
          {date ? date.toLocaleDateString() : "—"}
        </span>
      </p>
    </div>
  );
}

Headless usage

useDatePicker returns { state, ariaProps }. Spread ariaProps onto <AriaDatePicker> and compose field, trigger, popover, and calendar however you like.

The default container keeps the trigger in the same row as the field, overlapping the right edge (DatePickerField uses pe-9; the trigger uses negative margin). For a different layout—for example a full-width field and a separate full-width “open calendar” control underneath—override field padding (e.g. className="pe-3" on DatePickerField) and trigger width (className="ms-0 w-full …" on DatePickerTrigger) so spacing matches your design. Add a visible <Label> when it helps; the live demo below uses a stacked layout and custom chrome.

You can go further and skip f-ui pieces entirely: spread the same ariaProps onto DatePicker from react-aria-components, then compose Group, DateInput / DateSegment, Button, Popover, Dialog, and Calendar (and grid/cell primitives) with plain styling—no DatePickerField, DatePickerTrigger, DatePickerPopover, or Calendar from this library. Use the Code tab on each demo below for the full source instead of duplicating it here.

Headless · custom chrome

Selected:

"use client";

import { useState } from "react";
import {
  DatePicker as AriaDatePicker,
  I18nProvider,
  Label,
} from "react-aria-components";

import { Calendar } from "@/components/f-ui/date-internals/calendar-rac";
import { DatePickerField } from "@/components/f-ui/date-picker/date-picker-parts/date-picker-field";
import { DatePickerPopover } from "@/components/f-ui/date-picker/date-picker-parts/date-picker-popover";
import { DatePickerTrigger } from "@/components/f-ui/date-picker/date-picker-parts/date-picker-trigger";
import { useDatePicker } from "@/components/f-ui/date-picker/use-date-picker";

/**
 * Headless demo: same hook + parts as the default `DatePicker`, but a **different UI model** —
 * segmented field full width, calendar action as a **separate full-width control** under the field
 * (not overlapped on the right). Override field padding (`pe-3`) and trigger sizing (`w-full`)
 * to match this layout; the default container keeps the icon inside the field chrome.
 */
export function DatePickerHeadlessDemo() {
  const [date, setDate] = useState<Date | undefined>(undefined);
  const { ariaProps } = useDatePicker({ value: date ?? null, onChange: setDate });

  return (
    <I18nProvider locale="en-US">
      <AriaDatePicker
        {...ariaProps}
        className="flex max-w-md flex-col gap-3"
      >
        <div className="rounded-2xl border border-violet-500/25 bg-linear-to-br from-violet-500/[0.07] via-background to-cyan-500/6 p-4 shadow-sm dark:from-violet-500/15 dark:to-cyan-500/10">
          <Label className="text-violet-700 dark:text-violet-300 mb-3 block text-[11px] font-semibold tracking-[0.12em] uppercase">
            Headless · custom chrome
          </Label>
          <div className="flex flex-col gap-2">
            <DatePickerField className="rounded-xl border-0 bg-background/90 pe-3 shadow-inner ring-1 ring-border" />
            <DatePickerTrigger
              aria-label="Open calendar"
              className="ms-0 h-10 w-full min-w-0 shrink-0 justify-center gap-2 rounded-xl border border-violet-500/35 bg-violet-500/12 text-sm font-medium text-violet-800 shadow-none hover:bg-violet-500/20 dark:text-violet-200"
            />
          </div>
        </div>
        <DatePickerPopover className="rounded-2xl shadow-xl ring-1 ring-violet-500/20">
          <Calendar className="rounded-xl" />
        </DatePickerPopover>
      </AriaDatePicker>
      <p className="text-muted-foreground mt-1 text-xs">
        Selected:{" "}
        <span className="text-foreground font-medium tabular-nums">
          {date ? date.toLocaleDateString() : "—"}
        </span>
      </p>
    </I18nProvider>
  );
}
Headless · RAC only (no f-ui parts)

Selected:

"use client";

import { useState } from "react";
import {
  Button,
  Calendar,
  CalendarCell,
  CalendarGrid,
  CalendarGridBody,
  CalendarGridHeader,
  CalendarHeaderCell,
  DateInput,
  DatePicker as AriaDatePicker,
  DateSegment,
  Dialog,
  Group,
  Heading,
  I18nProvider,
  Label,
  Popover,
} from "react-aria-components";

import { useDatePicker } from "@/components/f-ui/date-picker/use-date-picker";

/**
 * Same hook as the shipped `DatePicker`, but **no f-ui parts**: only `react-aria-components`
 * and minimal styling on plain elements (native button via RAC `Button`, segmented field,
 * table-based calendar grid).
 */
export function DatePickerHtmlDemo() {
  const [date, setDate] = useState<Date | undefined>(undefined);
  const { ariaProps } = useDatePicker({ value: date ?? null, onChange: setDate });

  return (
    <I18nProvider locale="en-US">
      <AriaDatePicker {...ariaProps} className="flex max-w-md flex-col gap-2">
        <Label className="text-foreground block text-sm font-medium">
          Headless · RAC only (no f-ui parts)
        </Label>

        <Group className="flex flex-wrap items-center gap-2">
          <DateInput className="text-foreground inline-flex min-h-9 min-w-0 flex-1 items-center rounded border border-neutral-400 bg-white px-2 py-1 font-mono text-sm dark:border-neutral-600 dark:bg-neutral-950">
            {(segment) => (
              <DateSegment
                segment={segment}
                className="data-[type=literal]:text-neutral-500 data-placeholder:text-neutral-400 rounded px-0.5 caret-transparent outline-none focus:bg-neutral-200 data-[type=literal]:px-0 dark:focus:bg-neutral-800"
              />
            )}
          </DateInput>

          <Button
            type="button"
            aria-label="Open calendar"
            className="h-9 shrink-0 rounded border border-neutral-400 bg-neutral-100 px-3 text-sm dark:border-neutral-600 dark:bg-neutral-900"
          >
            Calendar
          </Button>
        </Group>

        <Popover
          offset={6}
          className="text-foreground rounded border border-neutral-400 bg-white shadow-md dark:border-neutral-600 dark:bg-neutral-950"
        >
          <Dialog className="p-2">
            <Calendar>
              <header className="mb-1 flex items-center gap-1">
                <Button
                  slot="previous"
                  type="button"
                  className="rounded border border-transparent px-2 py-1 text-lg leading-none hover:bg-neutral-100 dark:hover:bg-neutral-800"
                >

                </Button>
                <Heading className="flex-1 text-center text-sm font-medium" />
                <Button
                  slot="next"
                  type="button"
                  className="rounded border border-transparent px-2 py-1 text-lg leading-none hover:bg-neutral-100 dark:hover:bg-neutral-800"
                >

                </Button>
              </header>
              <CalendarGrid>
                <CalendarGridHeader>
                  {(day) => (
                    <CalendarHeaderCell className="text-neutral-500 size-8 p-0 text-center text-xs">
                      {day}
                    </CalendarHeaderCell>
                  )}
                </CalendarGridHeader>
                <CalendarGridBody>
                  {(d) => (
                    <CalendarCell
                      date={d}
                      className="p-0.5 text-center text-sm data-disabled:opacity-30 data-unavailable:line-through data-unavailable:opacity-40 data-selected:bg-neutral-800 data-selected:text-white dark:data-selected:bg-neutral-200 dark:data-selected:text-neutral-900"
                    />
                  )}
                </CalendarGridBody>
              </CalendarGrid>
            </Calendar>
          </Dialog>
        </Popover>
      </AriaDatePicker>

      <p className="text-muted-foreground mt-1 text-xs">
        Selected:{" "}
        <span className="text-foreground font-medium tabular-nums">
          {date ? date.toLocaleDateString() : "—"}
        </span>
      </p>
    </I18nProvider>
  );
}

Composition

DatePicker (AriaDatePicker)
├── Label (optional)
├── div (flex row)
│   ├── DatePickerField (Group + DateInput)
│   └── DatePickerTrigger (Button + CalendarIcon)
├── DatePickerPopover (Popover + Dialog)
│   └── Calendar
└── helper text (optional, from `placeholder`)

API Reference

Props

PropTypeDefault
valueDate | null
onChange(date?: Date) => void
onBlur() => void
localestring (BCP 47)
minValueDate
maxValueDate
isDateUnavailable(date: Date) => boolean
firstDayOfWeek"sun" | "mon" | ... | "sat"locale default
disabledbooleanfalse
isInvalidboolean
aria-invalidboolean
labelReact.ReactNode
aria-labelstring"Date" when label is omitted
placeholderstring (helper copy under the field)
classNamestring (outer wrapper)
classNamesPartial<Record<DatePickerSlot, string>>
calendarTriggerAriaLabelstring"Open calendar"

Slots (classNames)

SlotElement
rootOuter <AriaDatePicker> wrapper
field<DatePickerField> (segmented input group)
trigger<DatePickerTrigger> (calendar button)
popover<DatePickerPopover> (positioned panel)
calendar<Calendar>

Hook — useDatePicker(options)

OptionType
valueDate | null
onChange(date?: Date) => void
onBlur() => void
minValue / maxValueDate
isDateUnavailable(date: Date) => boolean
disabledboolean
isInvalid / aria-invalidboolean

Returns { state: { hasValue, isInvalid }, ariaProps }. Spread ariaProps onto <AriaDatePicker> from react-aria-components.

For accessibility details on the underlying primitives, see React Aria DatePicker.

On this page