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-pickerManual
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.
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:
closeOnSelectis automatically ignored whenselectionModeis"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.