Skip to content

Instantly share code, notes, and snippets.

@lanzclintonv
Last active April 14, 2025 11:28
Show Gist options
  • Save lanzclintonv/299f8738b8dc6dec85d15c51d4bd92b2 to your computer and use it in GitHub Desktop.
Save lanzclintonv/299f8738b8dc6dec85d15c51d4bd92b2 to your computer and use it in GitHub Desktop.
Shadcn Zag Datepicker
"use client";
import { useId } from "react";
import * as datepicker from "@zag-js/date-picker";
import { useMachine, normalizeProps } from "@zag-js/react";
import { X, ChevronLeft, CalendarIcon, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/cn";
type DatePickerProps = Omit<datepicker.Props, "id"> & {
className?: string;
};
export function DatePicker({ className, ...props }: DatePickerProps) {
const selectionMode: datepicker.SelectionMode = props?.selectionMode || "single";
const defaultFormat = (date: datepicker.DateValue, details: datepicker.LocaleDetails) =>
date.toDate(details.timeZone).toLocaleDateString("en-US", { day: "numeric", month: "long", year: "numeric" });
const service = useMachine(datepicker.machine, {
...props,
id: useId(),
selectionMode,
format: props?.format || defaultFormat,
});
const api = datepicker.connect(service, normalizeProps);
const displayValue =
{
single: () => api.valueAsString[0],
range: () => {
const [start, end] = api.valueAsString;
if (!start) return "Select Dates";
return end ? `${start} - ${end}` : start;
},
multiple: () => api.valueAsString.join(", "),
}[selectionMode]?.() || "";
return (
<Popover
open={api.open}
onOpenChange={(open) => {
api.setOpen(open);
api.setView(props?.minView || "day");
}}
>
<PopoverTrigger asChild>
<div {...api.getControlProps()}>
<Button
asChild
variant="outline"
onChange={api.getTriggerProps().onChange}
className={cn(
"relative min-w-[240px] justify-start text-left font-normal",
(!api.value || api.value.length === 0) && "text-muted-foreground",
className
)}
>
<div>
<CalendarIcon className="mr-2 size-4" />
{displayValue || "Select Date"}
{displayValue && (
<div
onClick={api.clearValue}
className="absolute top-1/2 right-2 -translate-y-1/2"
>
<Button
size="sm"
type="button"
variant="ghost"
className="h-5 w-5 p-0"
>
<X className="size-4" />
</Button>
</div>
)}
</div>
</Button>
</div>
</PopoverTrigger>
<PopoverContent
align="start"
className="p-0"
>
<div className="rounded-md p-4">
{/* Day View */}
<div
className="w-full"
hidden={api.view !== "day"}
>
<div
{...api.getViewControlProps({ view: "year" })}
className="mb-4 flex items-center justify-between"
>
<Button
{...api.getPrevTriggerProps()}
size="icon"
variant="ghost"
className="size-8 rounded-md border-2"
>
<ChevronLeft className="size-4" />
</Button>
<Button
{...api.getViewTriggerProps()}
variant="ghost"
className="font-medium"
>
{api.visibleRangeText.start}
</Button>
<Button
{...api.getNextTriggerProps()}
size="icon"
variant="ghost"
className="size-8 rounded-md border-2"
>
<ChevronRight className="size-4" />
</Button>
</div>
<table
{...api.getTableProps({ view: "day" })}
className="w-full border-collapse"
>
<thead {...api.getTableHeaderProps({ view: "day" })}>
<tr {...api.getTableRowProps({ view: "day" })}>
{api.weekDays.map((day, i) => (
<th
key={i}
scope="col"
aria-label={day.long}
className="text-muted-foreground pb-2 text-center text-xs font-medium"
>
{day.narrow}
</th>
))}
</tr>
</thead>
<tbody {...api.getTableBodyProps({ view: "day" })}>
{api.weeks.map((week, i) => (
<tr
key={i}
{...api.getTableRowProps({ view: "day" })}
>
{week.map((value, i) => (
<td
key={i}
{...api.getDayTableCellProps({ value })}
className="p-0 text-center"
>
<div
{...api.getDayTableCellTriggerProps({ value })}
className={cn(
"flex h-9 w-9 items-center justify-center rounded-md text-sm",
"hover:bg-accent",
"data-selected:bg-primary data-selected:text-primary-foreground",
"data-disabled:pointer-events-none data-disabled:opacity-50",
"data-in-range:bg-accent",
"[&:not([data-range-start],[data-range-end])]:data-in-range:rounded-none",
"data-range-start:rounded-r-none",
"data-range-end:rounded-l-none"
)}
>
{value.day}
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{/* Month View */}
<div
className="w-full"
hidden={api.view !== "month"}
>
<div
{...api.getViewControlProps({ view: "month" })}
className="mb-4 flex items-center justify-between"
>
<Button
{...api.getPrevTriggerProps({ view: "month" })}
size="icon"
variant="ghost"
className="size-8 rounded-md border-2"
>
<ChevronLeft className="size-4" />
</Button>
<Button
{...api.getViewTriggerProps({ view: "month" })}
variant="ghost"
className="font-medium"
>
{api.visibleRange.start.year}
</Button>
<Button
{...api.getNextTriggerProps({ view: "month" })}
size="icon"
variant="ghost"
className="size-8 rounded-md border-2"
>
<ChevronRight className="size-4" />
</Button>
</div>
<table
{...api.getTableProps({ view: "month", columns: 3 })}
className="w-full border-collapse"
>
<tbody {...api.getTableBodyProps({ view: "month" })}>
{api.getMonthsGrid({ columns: 3, format: "short" }).map((months, row) => (
<tr
key={row}
{...api.getTableRowProps()}
>
{months.map((month, index) => (
<td
key={index}
{...api.getMonthTableCellProps({
...month,
columns: 3,
})}
className="p-1 text-center"
>
<div
{...api.getMonthTableCellTriggerProps({
...month,
columns: 3,
})}
className={cn(
"flex h-9 w-16 items-center justify-center rounded-md text-sm",
"hover:bg-accent",
"data-selected:bg-primary data-selected:text-primary-foreground",
"data-disabled:pointer-events-none data-disabled:opacity-50"
)}
>
{month.label}
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{/* Year View */}
<div
className="w-full"
hidden={api.view !== "year"}
>
<div
{...api.getViewControlProps({ view: "year" })}
className="mb-4 flex items-center justify-between"
>
<Button
{...api.getPrevTriggerProps({ view: "year" })}
size="icon"
variant="ghost"
className="size-8 rounded-md border-2"
>
<ChevronLeft className="size-4" />
</Button>
<span className="text-sm font-medium">
{api.getDecade().start} - {api.getDecade().end}
</span>
<Button
{...api.getNextTriggerProps({ view: "year" })}
size="icon"
variant="ghost"
className="size-8 rounded-md border-2"
>
<ChevronRight className="size-4" />
</Button>
</div>
<table
{...api.getTableProps({ view: "year", columns: 3 })}
className="w-full border-collapse"
>
<tbody {...api.getTableBodyProps()}>
{api.getYearsGrid({ columns: 3 }).map((years, row) => (
<tr
key={row}
{...api.getTableRowProps({ view: "year" })}
>
{years.map((year, index) => (
<td
key={index}
{...api.getYearTableCellProps({
...year,
columns: 3,
})}
className="p-1 text-center"
>
<div
{...api.getYearTableCellTriggerProps({
...year,
columns: 3,
})}
className={cn(
"flex h-9 w-16 items-center justify-center rounded-md text-sm",
"hover:bg-accent",
"data-selected:bg-primary data-selected:text-primary-foreground",
"data-disabled:pointer-events-none data-disabled:opacity-50"
)}
>
{year.label}
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</PopoverContent>
</Popover>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment