| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 1 | 2 | 3 |
<DatePickerRoot selectionMode="range">
<DatePickerControl />
<DatePickerContent>
<DatePickerCalendar />
</DatePickerContent>
</DatePickerRoot>
npx @left4code/cosmic-ui-cli@latest add date-picker
This writes the component and everything it imports, then installs the packages it needs. Run npx @left4code/cosmic-ui-cli@latest initfirst if you haven't already.
Install the following dependencies:
Daterangepicker, Calendar, and Datepicker are the same date-picker.tsx primitive (Zag ships one date-picker machine, not three) composed differently: this page is DatePickerRoot with selectionMode="range", which is what makes DatePickerControl render a second input for the end date and the grid highlight everything between the two picks. Only the day view is wired up here (no month/year picker view); the plain border is a placeholder for the Frame primitive. Copy and paste the following code into your project.
import { createContext, useContext, useId } from "react";
import { twMerge } from "tailwind-merge";
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, X } from "lucide-react";
import { useMachine, normalizeProps } from "@zag-js/react";
import * as datePickerMachine from "@zag-js/date-picker";
import { Portal } from "@/components/ui/portal";
const DatePickerContext = createContext<ReturnType<typeof datePickerMachine.connect> | null>(
null
);
function useDatePickerContext() {
const api = useContext(DatePickerContext);
if (!api) throw new Error("DatePicker parts must be used within <DatePickerRoot>");
return api;
}
function DatePickerRoot({
children,
...rest
}: React.PropsWithChildren<Partial<datePickerMachine.Props>>) {
const service = useMachine(datePickerMachine.machine, { id: useId(), ...rest });
const api = datePickerMachine.connect(service, normalizeProps);
return <DatePickerContext.Provider value={api}>{children}</DatePickerContext.Provider>;
}
// The input+trigger row used by the popover variants (Datepicker,
// Daterangepicker). A plain inline <DatePickerCalendar> (Calendar) has no
// use for this - there's no text value to type or popover to open.
function DatePickerControl({ className }: { className?: string }) {
const api = useDatePickerContext();
return (
<div
{...api.getControlProps()}
className={twMerge([
"flex items-center gap-2 border border-primary/30 bg-primary/10 px-3 py-2 w-fit",
className,
])}
>
<input
{...api.getInputProps({ index: 0 })}
placeholder="Pick a date"
className="outline-none bg-transparent w-28"
/>
{api.selectionMode === "range" && (
<>
<span className="opacity-50">-</span>
<input
{...api.getInputProps({ index: 1 })}
placeholder="Pick a date"
className="outline-none bg-transparent w-28"
/>
</>
)}
<button
{...api.getClearTriggerProps()}
className="opacity-70 hover:opacity-100 cursor-pointer"
>
<X className="size-3.5" />
</button>
<button
{...api.getTriggerProps()}
className="opacity-70 hover:opacity-100 cursor-pointer"
>
<CalendarIcon className="size-4" />
</button>
</div>
);
}
// Wraps <DatePickerCalendar> in a floating popover. getContentProps() already
// manages its own `hidden` state, so unlike Dialog/Combobox this doesn't need
// a separate presence hook for an exit animation.
function DatePickerContent({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) {
const api = useDatePickerContext();
const positionerProps = api.getPositionerProps();
return (
<Portal>
<div {...positionerProps} style={{ ...positionerProps.style, zIndex: 70 }}>
<div
{...api.getContentProps()}
className={twMerge(["border border-primary/30 bg-background outline-none", className])}
>
{children}
</div>
</div>
</Portal>
);
}
// The day grid, with a prev/next month header. This is the one part shared
// verbatim across Calendar (rendered inline, no popover), Datepicker (single
// selection, inside a popover), and Daterangepicker (range selection, inside
// a popover) - only the selectionMode/inline props passed to DatePickerRoot
// differ between the three.
//
// Scoped to the day view only: Zag's machine also supports drilling into
// month/year picker views (view/minView/maxView, getMonthsGrid, etc.), left
// out here to keep this placeholder implementation a manageable size.
function DatePickerCalendar({ className }: { className?: string }) {
const api = useDatePickerContext();
return (
<div className={twMerge(["p-3", className])}>
<div className="flex items-center justify-between mb-3">
<button
{...api.getPrevTriggerProps()}
className="p-1 hover:bg-primary/10 disabled:opacity-30 disabled:pointer-events-none cursor-pointer"
>
<ChevronLeft className="size-4" />
</button>
<span className="font-medium">{api.visibleRangeText.formatted}</span>
<button
{...api.getNextTriggerProps()}
className="p-1 hover:bg-primary/10 disabled:opacity-30 disabled:pointer-events-none cursor-pointer"
>
<ChevronRight className="size-4" />
</button>
</div>
<table {...api.getTableProps({ view: "day" })} className="border-collapse">
<thead {...api.getTableHeadProps()}>
<tr {...api.getTableRowProps()}>
{api.weekDays.map((day, i) => (
<th key={i} scope="col" className="text-xs opacity-50 font-normal pb-2 size-8">
{day.narrow}
</th>
))}
</tr>
</thead>
<tbody {...api.getTableBodyProps()}>
{api.weeks.map((week, i) => (
<tr key={i} {...api.getTableRowProps()}>
{week.map((value, j) => (
<td key={j} {...api.getDayTableCellProps({ value })} className="text-center p-0.5">
<div
{...api.getDayTableCellTriggerProps({ value })}
className={twMerge([
"size-8 flex items-center justify-center cursor-pointer border border-transparent hover:border-primary/30",
"data-[selected]:bg-primary/20 data-[selected]:border-primary",
"data-[today]:border-primary/50",
"data-[in-range]:bg-primary/10",
"data-[outside-range]:opacity-30",
"data-[unavailable]:opacity-20 data-[unavailable]:pointer-events-none",
"data-[disabled]:opacity-20 data-[disabled]:pointer-events-none",
])}
>
{value.day}
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
export { DatePickerRoot, DatePickerControl, DatePickerContent, DatePickerCalendar };
Update the import paths to match your project setup.
import {
DatePickerRoot,
DatePickerControl,
DatePickerContent,
DatePickerCalendar,
} from "@/components/ui/date-picker";
<DatePickerRoot selectionMode="range">
<DatePickerControl />
<DatePickerContent>
<DatePickerCalendar />
</DatePickerContent>
</DatePickerRoot>