Date Picker
Single-date and date-range selection with shortcuts, manual confirmation, localized calendars and editable date fields.
Give your AI assistant this page as context.
Connect your library with MCPui/date-pickerUsage
Get this component from a saved library export or the connected CLI. 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 covers runtime dependencies and React/Next.js setup.
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().
<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.
<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.
<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.
<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.
Icons use Lucide React. Install lucide-react@1.47.0 alongside React and Base UI.
Dependencies: Calendar, 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.
"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>
);
}