Date Picker

A date picker component that allows users to select a date or date range from a calendar popup.

import { DatePicker } from "~/components/date-picker";

export default function DatePickerBasicDemo() {
  return <DatePicker label="Birth date" />;
}

Installation

CLI

Run the following command to add the component to your project:

npx @ark-preset/cli@latest add date-picker

Manual

Create the recipe file at src/components/recipes/date-picker.ts:

import { tv, type VariantProps } from "tailwind-variants";

export const datePickerVariants = tv({
  slots: {
    root: "w-full flex flex-col gap-1.5",
    control: "flex w-full items-center gap-1 flex-wrap",
    input:
      "flex-1 h-8 rounded-md border border-input bg-background px-2.5 py-1.5 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50",
    trigger: [
      "flex min-h-8 min-w-8 items-center justify-center rounded-md border border-border bg-background",
      "transition duration-300 motion-safe:hover:-translate-y-0.5 motion-safe:active:translate-y-0.5 active:duration-150 hover:bg-accent/50",
      "focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring",
      "disabled:cursor-not-allowed disabled:opacity-50",
      "[&>svg]:size-4",
    ],
    clearTrigger: [
      "flex min-h-8 items-center justify-center rounded-md border border-border bg-background px-3 text-sm",
      "transition duration-300 motion-safe:hover:-translate-y-0.5 motion-safe:active:translate-y-0.5 active:duration-150 hover:bg-accent/50",
      "focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring",
      "disabled:cursor-not-allowed disabled:opacity-50",
    ],
    content: [
      "z-50 rounded-md border border-border bg-popover p-3 text-popover-foreground shadow-md outline-none",
      "data-[state=open]:animate-in data-[state=closed]:animate-out",
      "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
      "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
    ],
    view: "space-y-4",
    viewControl: "flex items-center justify-between gap-4",
    navTrigger: "size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
    viewTrigger: "h-7",
    rangeText: "text-sm font-medium",
    table: "w-full border-collapse space-y-1",
    tableRow: "mt-2 flex w-full",
    tableHeader: "w-8 flex-1 text-[0.8rem] font-normal text-muted-foreground",
    tableCell: [
      "flex-1 p-0 text-center text-sm",
      "has-[[data-range-end]]:rounded-r-md has-[[data-range-start]]:rounded-l-md",
      "has-[[data-in-range]]:bg-accent",
      "has-[[data-outside-range][data-in-range]]:bg-accent/50",
      "has-[[data-in-range]]:first-of-type:rounded-l-md has-[[data-in-range]]:last-of-type:rounded-r-md",
    ],
    tableCellTrigger: [
      "py-1 px-2 font-normal data-[selected]:opacity-100",
      "data-[today]:bg-accent data-[today]:text-accent-foreground",
      "[&:is([data-today][data-selected])]:bg-primary [&:is([data-today][data-selected])]:text-primary-foreground",
      "data-[selected]:bg-primary data-[selected]:text-primary-foreground",
      "data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground",
      "data-[disabled]:text-muted-foreground data-[disabled]:opacity-50",
      "data-[outside-range]:text-muted-foreground data-[outside-range]:opacity-50",
      "[&:is([data-outside-range][data-in-range])]:bg-accent/50",
      "[&:is([data-outside-range][data-in-range])]:text-muted-foreground",
      "[&:is([data-outside-range][data-in-range])]:opacity-30",
    ],
    selectedValue:
      "flex flex-wrap items-center gap-1.5 flex-1 min-w-0 min-h-8 rounded-md border border-input bg-background px-2.5 py-1 text-sm",
    selectedValuePlaceholder: "text-sm text-muted-foreground",
    selectedValueTag: [
      "inline-flex items-center gap-1 rounded-md bg-primary/10 ps-2 pe-0.5 py-0.5 text-xs font-medium text-primary",
      "data-[disabled]:opacity-50",
    ],
    selectedValueRemove: [
      "inline-flex items-center justify-center size-4 rounded-sm",
      "text-primary hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring",
    ],
  },
  variants: {
    error: {
      true: {
        control: "border-destructive focus-within:ring-destructive",
        input: "border-destructive focus-visible:ring-destructive",
        selectedValue: "border-destructive",
      },
    },
  },
  defaultVariants: {
    error: false,
  },
});

export type DatePickerVariants = VariantProps<typeof datePickerVariants>;

Create the component directory and files.

src/components/date-picker/date-picker.base.tsx:

import { DatePicker as ArkDatePicker } from "@ark-ui/solid/date-picker";
import { splitProps, type Component } from "solid-js";
import { datePickerVariants, labelVariants } from "../recipes/date-picker";

const styles = datePickerVariants();

const Root: Component<ArkDatePicker.RootProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDatePicker.Root class={styles.root({ class: local.class })} {...others} />;
};
const RootProvider = ArkDatePicker.RootProvider;
const Label: Component<ArkDatePicker.LabelProps & { error?: boolean }> = (props) => {
  const [local, others] = splitProps(props, ["class", "error"]);
  return (
    <ArkDatePicker.Label
      class={labelVariants({ class: local.class, error: local.error })}
      {...others}
    />
  );
};
const Context = ArkDatePicker.Context;
const TableHead = ArkDatePicker.TableHead;
const TableBody = ArkDatePicker.TableBody;
const YearSelect = ArkDatePicker.YearSelect;
const MonthSelect = ArkDatePicker.MonthSelect;

// Pass-through positioned portal container
const Positioner = ArkDatePicker.Positioner;

// Styled wrapper components (tv() recipe styling)
const Control: Component<ArkDatePicker.ControlProps & { error?: boolean }> = (props) => {
  const [local, others] = splitProps(props, ["class", "error"]);
  return (
    <ArkDatePicker.Control
      class={styles.control({ class: local.class, error: local.error })}
      {...others}
    />
  );
};

const Input: Component<ArkDatePicker.InputProps & { error?: boolean }> = (props) => {
  const [local, others] = splitProps(props, ["class", "error"]);
  return (
    <ArkDatePicker.Input
      class={styles.input({ class: local.class, error: local.error })}
      {...others}
    />
  );
};

const Content: Component<ArkDatePicker.ContentProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDatePicker.Content class={styles.content({ class: local.class })} {...others} />;
};

const View: Component<ArkDatePicker.ViewProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDatePicker.View class={styles.view({ class: local.class })} {...others} />;
};

const ViewControl: Component<ArkDatePicker.ViewControlProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkDatePicker.ViewControl class={styles.viewControl({ class: local.class })} {...others} />
  );
};

const RangeText: Component<ArkDatePicker.RangeTextProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDatePicker.RangeText class={styles.rangeText({ class: local.class })} {...others} />;
};

const Table: Component<ArkDatePicker.TableProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDatePicker.Table class={styles.table({ class: local.class })} {...others} />;
};

const TableRow: Component<ArkDatePicker.TableRowProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDatePicker.TableRow class={styles.tableRow({ class: local.class })} {...others} />;
};

const TableHeader: Component<ArkDatePicker.TableHeaderProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkDatePicker.TableHeader class={styles.tableHeader({ class: local.class })} {...others} />
  );
};

const TableCell: Component<ArkDatePicker.TableCellProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDatePicker.TableCell class={styles.tableCell({ class: local.class })} {...others} />;
};

const Trigger: Component<ArkDatePicker.TriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDatePicker.Trigger class={styles.trigger({ class: local.class })} {...others} />;
};

const ClearTrigger: Component<ArkDatePicker.ClearTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkDatePicker.ClearTrigger class={styles.clearTrigger({ class: local.class })} {...others} />
  );
};

const PrevTrigger: Component<ArkDatePicker.PrevTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkDatePicker.PrevTrigger class={styles.navTrigger({ class: local.class })} {...others} />
  );
};

const NextTrigger: Component<ArkDatePicker.NextTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkDatePicker.NextTrigger class={styles.navTrigger({ class: local.class })} {...others} />
  );
};

const ViewTrigger: Component<ArkDatePicker.ViewTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkDatePicker.ViewTrigger class={styles.viewTrigger({ class: local.class })} {...others} />
  );
};

const TableCellTrigger: Component<ArkDatePicker.TableCellTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkDatePicker.TableCellTrigger
      class={styles.tableCellTrigger({ class: local.class })}
      {...others}
    />
  );
};

export const DatePickerBase = {
  Root,
  RootProvider,
  Label,
  Context,
  TableHead,
  TableBody,
  YearSelect,
  MonthSelect,
  Positioner,
  Control,
  Input,
  Content,
  View,
  ViewControl,
  RangeText,
  Table,
  TableRow,
  TableHeader,
  TableCell,
  Trigger,
  ClearTrigger,
  PrevTrigger,
  NextTrigger,
  ViewTrigger,
  TableCellTrigger,
};

src/components/date-picker/index.tsx:

import { For, Show, splitProps, type Component, type JSX } from "solid-js";
import { DatePicker as ArkDatePicker } from "@ark-ui/solid/date-picker";
import type { UseDatePickerContext } from "@ark-ui/solid/date-picker";
import type { DateValue } from "@internationalized/date";
import { Portal } from "solid-js/web";
import { buttonVariants, datePickerVariants } from "../recipes/date-picker";
import { DatePickerBase } from "./date-picker.base";

const styles = datePickerVariants();

// ── SVG Icon Components ──────────────────────────────────────

const CalendarIcon: Component<JSX.SvgSVGAttributes<SVGSVGElement>> = (props) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    class="size-4"
    {...props}
  >
    <path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12z" />
    <path d="M16 3v4" />
    <path d="M8 3v4" />
    <path d="M4 11h16" />
    <path d="M11 15h1" />
    <path d="M12 15v3" />
    <title>Calendar</title>
  </svg>
);

const ChevronLeftIcon: Component<JSX.SvgSVGAttributes<SVGSVGElement>> = (props) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    class="size-4"
    {...props}
  >
    <path d="M15 6l-6 6l6 6" />
    <title>Previous</title>
  </svg>
);

const ChevronRightIcon: Component<JSX.SvgSVGAttributes<SVGSVGElement>> = (props) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    class="size-4"
    {...props}
  >
    <path d="M9 6l6 6l-6 6" />
    <title>Next</title>
  </svg>
);

const XIcon: Component<JSX.SvgSVGAttributes<SVGSVGElement>> = (props) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    class="size-3"
    {...props}
  >
    <path d="M18 6l-12 12" />
    <path d="M6 6l12 12" />
    <title>Remove</title>
  </svg>
);

// ── CalendarGridView (internal) ──────────────────────────────

const CalendarGridView: Component<{ view: "day" | "month" | "year" }> = (props) => (
  <DatePickerBase.View view={props.view}>
    <DatePickerBase.Context>
      {(ctx: UseDatePickerContext) => (
        <>
          <DatePickerBase.ViewControl>
            <DatePickerBase.PrevTrigger class={buttonVariants({ variant: "outline" })}>
              <ChevronLeftIcon />
            </DatePickerBase.PrevTrigger>
            <DatePickerBase.ViewTrigger class={buttonVariants({ variant: "ghost" })}>
              <DatePickerBase.RangeText />
            </DatePickerBase.ViewTrigger>
            <DatePickerBase.NextTrigger class={buttonVariants({ variant: "outline" })}>
              <ChevronRightIcon />
            </DatePickerBase.NextTrigger>
          </DatePickerBase.ViewControl>
          <DatePickerBase.Table>
            <Show when={props.view === "day"}>
              <DatePickerBase.TableHead>
                <DatePickerBase.TableRow>
                  {ctx().weekDays.map((weekDay: { short: string }) => (
                    <DatePickerBase.TableHeader>{weekDay.short}</DatePickerBase.TableHeader>
                  ))}
                </DatePickerBase.TableRow>
              </DatePickerBase.TableHead>
            </Show>
            <DatePickerBase.TableBody>
              <Show when={props.view === "day"}>
                {ctx().weeks.map((week: DateValue[]) => (
                  <DatePickerBase.TableRow>
                    {week.map((day: DateValue) => (
                      <DatePickerBase.TableCell value={day}>
                        <DatePickerBase.TableCellTrigger
                          class={buttonVariants({
                            variant: "ghost",
                            class: "aspect-square px-0",
                          })}
                        >
                          {day.day}
                        </DatePickerBase.TableCellTrigger>
                      </DatePickerBase.TableCell>
                    ))}
                  </DatePickerBase.TableRow>
                ))}
              </Show>
              <Show when={props.view === "month"}>
                {ctx()
                  .getMonthsGrid({ columns: 4, format: "short" })
                  .map((months: { value: number; label: string }[]) => (
                    <DatePickerBase.TableRow>
                      {months.map((month) => (
                        <DatePickerBase.TableCell value={month.value}>
                          <DatePickerBase.TableCellTrigger
                            class={buttonVariants({
                              variant: "ghost",
                            })}
                          >
                            {month.label}
                          </DatePickerBase.TableCellTrigger>
                        </DatePickerBase.TableCell>
                      ))}
                    </DatePickerBase.TableRow>
                  ))}
              </Show>
              <Show when={props.view === "year"}>
                {ctx()
                  .getYearsGrid({ columns: 4 })
                  .map((years: { value: number; label: string }[]) => (
                    <DatePickerBase.TableRow>
                      {years.map((year) => (
                        <DatePickerBase.TableCell value={year.value}>
                          <DatePickerBase.TableCellTrigger
                            class={buttonVariants({
                              variant: "ghost",
                            })}
                          >
                            {year.label}
                          </DatePickerBase.TableCellTrigger>
                        </DatePickerBase.TableCell>
                      ))}
                    </DatePickerBase.TableRow>
                  ))}
              </Show>
            </DatePickerBase.TableBody>
          </DatePickerBase.Table>
        </>
      )}
    </DatePickerBase.Context>
  </DatePickerBase.View>
);

// ── Internal: SingleControl ──────────────────────────────────

const SingleControl: Component<{
  placeholder?: string;
  clearLabel?: string | JSX.Element;
  error?: boolean;
}> = (props) => (
  <>
    <DatePickerBase.Input placeholder={props.placeholder} error={props.error} />
    <DatePickerBase.Trigger>
      <CalendarIcon />
    </DatePickerBase.Trigger>
    <DatePickerBase.ClearTrigger>{props.clearLabel ?? "Clear"}</DatePickerBase.ClearTrigger>
  </>
);

// ── Internal: RangeControl ───────────────────────────────────

const RangeControl: Component<{
  clearLabel?: string | JSX.Element;
  error?: boolean;
}> = (props) => (
  <>
    <DatePickerBase.Input placeholder="Start date" error={props.error} index={0} />
    <span class="text-sm text-muted-foreground select-none hidden sm:inline-block">—</span>
    <DatePickerBase.Input placeholder="End date" error={props.error} index={1} />
    <div class="flex w-full sm:w-fit gap-1">
      <DatePickerBase.Trigger>
        <CalendarIcon />
      </DatePickerBase.Trigger>
      <DatePickerBase.ClearTrigger>{props.clearLabel ?? "Clear"}</DatePickerBase.ClearTrigger>
    </div>
  </>
);

// ── Internal: MultipleControl ────────────────────────────────

const MultipleControl: Component<{
  placeholder?: string;
  clearLabel?: string | JSX.Element;
  error?: boolean;
}> = (props) => (
  <DatePickerBase.Context>
    {(ctx: UseDatePickerContext) => (
      <>
        <div class={styles.selectedValue()}>
          <Show
            when={ctx().value.length > 0}
            fallback={
              <span class={styles.selectedValuePlaceholder()}>
                {props.placeholder || "Select dates..."}
              </span>
            }
          >
            <For each={ctx().value}>
              {(date: DateValue, index) => {
                const fmt = (d: DateValue) =>
                  d.toDate("UTC").toLocaleDateString("en-US", {
                    month: "short",
                    day: "numeric",
                    year: "numeric",
                  });
                return (
                  <span class={styles.selectedValueTag()}>
                    {fmt(date)}
                    <button
                      class={styles.selectedValueRemove()}
                      onClick={() => ctx().setValue(ctx().value.filter((_, i) => i !== index()))}
                      aria-label={`Remove ${fmt(date)}`}
                    >
                      <XIcon />
                    </button>
                  </span>
                );
              }}
            </For>
          </Show>
        </div>
        <DatePickerBase.Trigger>
          <CalendarIcon />
        </DatePickerBase.Trigger>
        <DatePickerBase.ClearTrigger>{props.clearLabel ?? "Clear"}</DatePickerBase.ClearTrigger>
      </>
    )}
  </DatePickerBase.Context>
);

// ── Composite: DatePicker ────────────────────────────────────

type DatePickerProps = ArkDatePicker.RootProps & {
  label?: string;
  placeholder?: string;
  clearLabel?: string | JSX.Element;
  error?: boolean;
};

const DatePicker: Component<DatePickerProps> = (props) => {
  const [local, others] = splitProps(props, [
    "class",
    "label",
    "placeholder",
    "clearLabel",
    "error",
  ]);

  const selectionMode = () => props.selectionMode ?? "single";

  return (
    <DatePickerBase.Root
      class={local.class}
      positioning={{ placement: "bottom-end", ...props.positioning }}
      {...others}
    >
      {local.label && (
        <DatePickerBase.Label error={local.error}>{local.label}</DatePickerBase.Label>
      )}
      <DatePickerBase.Control error={local.error}>
        <Show when={selectionMode() === "single" || selectionMode() === undefined}>
          <SingleControl
            placeholder={local.placeholder}
            clearLabel={local.clearLabel}
            error={local.error}
          />
        </Show>
        <Show when={selectionMode() === "range"}>
          <RangeControl clearLabel={local.clearLabel} error={local.error} />
        </Show>
        <Show when={selectionMode() === "multiple"}>
          <MultipleControl
            placeholder={local.placeholder}
            clearLabel={local.clearLabel}
            error={local.error}
          />
        </Show>
      </DatePickerBase.Control>
      <Portal>
        <DatePickerBase.Positioner>
          <DatePickerBase.Content>
            <CalendarGridView view="day" />
            <CalendarGridView view="month" />
            <CalendarGridView view="year" />
          </DatePickerBase.Content>
        </DatePickerBase.Positioner>
      </Portal>
    </DatePickerBase.Root>
  );
};

// ── Exports ──────────────────────────────────────────────────

export { DatePicker, DatePickerBase };

export { datePickerVariants, type DatePickerVariants } from "../recipes/date-picker";

Note: Make sure your project has the Tailwind CSS theme variables set up (--background, --foreground, --ring, --border, etc.) or override the utility classes to match your design system.

Dependencies: This component imports shared recipes or sub-components from other packages. Make sure the following are also installed: button.

Usage

Basic Usage

import { DatePicker } from "~/components/date-picker";

export default function DatePickerBasicDemo() {
  return <DatePicker label="Birth date" />;
}

Selection Modes

The date picker supports three selection modes via the selectionMode prop:

Single (default)

Select one date at a time. This is the default mode.

import { DatePicker } from "~/components/date-picker";

export default function DatePickerBasicDemo() {
  return <DatePicker label="Birth date" />;
}

Range

Select a start and end date. The control renders two inputs — one for each end of the range.

import { DatePicker } from "~/components/date-picker";

export default function DatePickerRangeDemo() {
  return <DatePicker selectionMode="range" label="Date range" placeholder="Start date" />;
}

Multiple

Select multiple individual dates. The control renders selected dates as removable chips.

Select dates...
import { DatePicker } from "~/components/date-picker";

export default function DatePickerMultipleDemo() {
  return (
    <DatePicker selectionMode="multiple" label="Multiple dates" placeholder="Select dates..." />
  );
}

Use maxSelectedDates to limit the number of selections in multiple mode:

<DatePicker
  selectionMode="multiple"
  maxSelectedDates={3}
  label="Up to 3 dates"
/>

Note: closeOnSelect is automatically ignored when selectionMode is "multiple" (Ark UI behavior).

Controlled

Control the selected date programmatically using value and onValueChange.

import { createSignal } from "solid-js";
import { DatePicker } from "~/components/date-picker";
import { type DateValue } from "@ark-ui/solid/date-picker";

export default function DatePickerControlledDemo() {
  const [value, setValue] = createSignal<DateValue[]>([]);

  return (
    <DatePicker
      label="Select date"
      value={value()}
      onValueChange={(details) => setValue(details.value)}
    />
  );
}

API Reference

See the Ark UI Date Picker documentation.