Calendar
A calendar component that allows users to select a date or a range of dates.
The Calendar component is built on top of Corvu Calendar, which provides the accessible calendar primitive and keyboard navigation.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { createSignal } from "solid-js";import { Calendar } from "~/components/ui/calendar";
export default function CalendarDemo() { const [date, setDate] = createSignal(new Date());
return ( <Calendar mode="single" selected={date()} onSelect={setDate} class="rounded-lg border" captionLayout="dropdown" /> );}Installation
Usage
import { createSignal } from "solid-js";import { Calendar } from "~/components/ui/calendar";
const [date, setDate] = createSignal<Date>();<Calendar mode="single" selected={date()} onSelect={setDate} class="rounded-lg border"/>See the Corvu Calendar documentation for primitive-level options and behavior.
Date Picker
You can use the Calendar component to build a date picker. Zaidan does not
yet ship a dedicated Date Picker page or registry item — Date Picker is
excluded from the shadcn sync and tracked separately.
Persian Locale
This focused demo renders and selects Persian calendar dates.
۱۴۰۴ خرداد
| شنبه | یکشنبه | دوشنبه | سهشنبه | چهارشنبه | پنجشنبه | جمعه |
|---|---|---|---|---|---|---|
import { CalendarDate, PersianCalendar, startOfWeek } from "@internationalized/date";import { ChevronLeftIcon, ChevronRightIcon } from "lucide-solid";import { createMemo, createSignal, For } from "solid-js";import { Button } from "~/components/ui/button";
const persianCalendar = new PersianCalendar();const numberFormatter = new Intl.NumberFormat("fa-IR-u-nu-arabext", { useGrouping: false });const monthFormatter = new Intl.DateTimeFormat("fa-IR-u-ca-persian", { month: "long", year: "numeric", timeZone: "UTC",});const dayFormatter = new Intl.DateTimeFormat("fa-IR-u-ca-persian", { dateStyle: "full", timeZone: "UTC",});const weekdayFormatter = new Intl.DateTimeFormat("fa-IR-u-ca-persian", { weekday: "short", timeZone: "UTC",});
const sameDay = (left: CalendarDate, right: CalendarDate) => left.compare(right) === 0;
export default function CalendarHijri() { const [month, setMonth] = createSignal(new CalendarDate(persianCalendar, 1404, 3, 1)); const [selected, setSelected] = createSignal(new CalendarDate(persianCalendar, 1404, 3, 22)); const focusDay = (day: CalendarDate) => { const target = document.querySelector<HTMLButtonElement>( `[data-persian-day="${day.toString()}"]`, ); target?.focus(); }; const onDayKeyDown = (day: CalendarDate, event: KeyboardEvent) => { const offsets: Record<string, number> = { ArrowDown: 7, ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, }; const offset = offsets[event.key];
if (offset === undefined) return; event.preventDefault(); focusDay(day.add({ days: offset })); }; const weekdays = createMemo(() => { const firstDay = startOfWeek(month(), "fa-IR");
return Array.from({ length: 7 }, (_, index) => weekdayFormatter.format(firstDay.add({ days: index }).toDate("UTC")), ); }); const days = createMemo(() => { const start = startOfWeek(month(), "fa-IR"); return Array.from({ length: 42 }, (_, index) => start.add({ days: index })); });
return ( <div lang="fa" class="w-fit rounded-lg border bg-background p-3 [--cell-size:--spacing(8)]"> <div class="mb-4 flex h-(--cell-size) items-center justify-between gap-1"> <Button type="button" variant="ghost" size="icon" aria-label="ماه قبل" onClick={() => setMonth((value) => value.subtract({ months: 1 }))} > <ChevronRightIcon /> </Button> <p class="text-sm font-medium">{monthFormatter.format(month().toDate("UTC"))}</p> <Button type="button" variant="ghost" size="icon" aria-label="ماه بعد" onClick={() => setMonth((value) => value.add({ months: 1 }))} > <ChevronLeftIcon /> </Button> </div> <table class="w-full border-collapse"> <caption class="sr-only">تقویم فارسی</caption> <thead> <tr class="flex"> <For each={weekdays()}> {(weekday) => ( <th scope="col" class="flex-1 text-center text-[0.8rem] font-normal text-muted-foreground" > {weekday} </th> )} </For> </tr> </thead> <tbody> <For each={Array.from({ length: 6 })}> {(_, weekIndex) => ( <tr class="mt-2 flex w-full"> <For each={days().slice(weekIndex() * 7, weekIndex() * 7 + 7)}> {(day) => { const outside = () => day.month !== month().month; const isSelected = () => sameDay(day, selected());
return ( <td class="relative flex-1 p-0 text-center"> <Button type="button" variant="ghost" size="icon" aria-label={dayFormatter.format(day.toDate("UTC"))} aria-selected={isSelected()} data-selected={isSelected() || undefined} data-persian-day={day.toString()} class="size-(--cell-size) font-normal data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground" classList={{ "text-muted-foreground": outside() }} onClick={() => setSelected(day)} onKeyDown={(event) => onDayKeyDown(day, event)} > {numberFormatter.format(day.day)} </Button> </td> ); }} </For> </tr> )} </For> </tbody> </table> </div> );}The primary Calendar wrapper accepts JavaScript Date values. This focused demo uses Solid's @internationalized/date Persian calendar for the alternate calendar arithmetic.
Selected Date (With Time Zone)
Pass timeZone to ensure the selected date uses the user's local time zone. Resolve it on mount when your page is server-rendered.
import { createSignal, onMount } from "solid-js";
const [date, setDate] = createSignal<Date>();const [timeZone, setTimeZone] = createSignal<string>();
onMount(() => { setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone);});
<Calendar mode="single" selected={date()} onSelect={setDate} timeZone={timeZone()}/>Basic
A basic calendar component styled with class="rounded-lg border".
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { Calendar } from "~/components/ui/calendar";
export default function CalendarBasic() { return <Calendar mode="single" class="rounded-lg border" />;}Range Calendar
Use mode="range" to enable range selection.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { addDays } from "date-fns";import { createSignal } from "solid-js";import { Calendar, type CalendarRangeValue } from "~/components/ui/calendar";
export default function CalendarRange() { const start = new Date(new Date().getFullYear(), 0, 12); const [dateRange, setDateRange] = createSignal<CalendarRangeValue>({ from: start, to: addDays(start, 30), });
return ( <Calendar mode="range" defaultMonth={dateRange().from} selected={dateRange()} onSelect={setDateRange} numberOfMonths={2} class="rounded-lg border" /> );}Month and Year Selector
Use captionLayout="dropdown" to show month and year selectors.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { Calendar } from "~/components/ui/calendar";
export default function CalendarCaption() { return <Calendar mode="single" captionLayout="dropdown" class="rounded-lg border" />;}Presets
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { addDays } from "date-fns";import { createSignal, For } from "solid-js";import { Button } from "~/components/ui/button";import { Calendar } from "~/components/ui/calendar";import { Card, CardContent, CardFooter } from "~/components/ui/card";
const presets = [ { label: "Today", value: 0 }, { label: "Tomorrow", value: 1 }, { label: "In 3 days", value: 3 }, { label: "In a week", value: 7 }, { label: "In 2 weeks", value: 14 },];
export default function CalendarPresets() { const [date, setDate] = createSignal(new Date(new Date().getFullYear(), 1, 12)); const [currentMonth, setCurrentMonth] = createSignal( new Date(new Date().getFullYear(), new Date().getMonth(), 1), );
const selectPreset = (offset: number) => { const nextDate = addDays(new Date(), offset); setDate(nextDate); setCurrentMonth(new Date(nextDate.getFullYear(), nextDate.getMonth(), 1)); };
return ( <Card class="mx-auto w-fit max-w-[300px]" size="sm"> <CardContent> <Calendar mode="single" selected={date()} onSelect={setDate} month={currentMonth()} onMonthChange={setCurrentMonth} fixedWeeks class="p-0 [--cell-size:--spacing(9.5)]" /> </CardContent> <CardFooter class="flex flex-wrap gap-2 border-t"> <For each={presets}> {(preset) => ( <Button variant="outline" size="sm" class="flex-1" onClick={() => selectPreset(preset.value)} > {preset.label} </Button> )} </For> </CardFooter> </Card> );}Date and Time Picker
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { Clock2Icon } from "lucide-solid";import { createSignal } from "solid-js";import { Calendar } from "~/components/ui/calendar";import { Card, CardContent, CardFooter } from "~/components/ui/card";import { Field, FieldGroup, FieldLabel } from "~/components/ui/field";import { InputGroup, InputGroupAddon, InputGroupInput } from "~/components/ui/input-group";
export default function CalendarTime() { const [date, setDate] = createSignal( new Date(new Date().getFullYear(), new Date().getMonth(), 12), );
return ( <Card size="sm" class="mx-auto w-fit"> <CardContent> <Calendar mode="single" selected={date()} onSelect={setDate} class="p-0" /> </CardContent> <CardFooter class="border-t bg-card"> <FieldGroup> <Field> <FieldLabel for="time-from">Start Time</FieldLabel> <InputGroup> <InputGroupInput id="time-from" type="time" step="1" value="10:30:00" class="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" /> <InputGroupAddon> <Clock2Icon class="text-muted-foreground" /> </InputGroupAddon> </InputGroup> </Field> <Field> <FieldLabel for="time-to">End Time</FieldLabel> <InputGroup> <InputGroupInput id="time-to" type="time" step="1" value="12:30:00" class="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" /> <InputGroupAddon> <Clock2Icon class="text-muted-foreground" /> </InputGroupAddon> </InputGroup> </Field> </FieldGroup> </CardFooter> </Card> );}Booked Dates
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { createSignal } from "solid-js";import { Calendar } from "~/components/ui/calendar";import { Card, CardContent } from "~/components/ui/card";
export default function CalendarBookedDates() { const initialDate = new Date(new Date().getFullYear(), 0, 6); const [date, setDate] = createSignal(initialDate); const bookedDates = Array.from( { length: 15 }, (_, index) => new Date(new Date().getFullYear(), 0, 12 + index), );
return ( <Card class="mx-auto w-fit p-0"> <CardContent class="p-0"> <Calendar mode="single" defaultMonth={date()} selected={date()} onSelect={setDate} disabled={bookedDates} modifiers={{ booked: bookedDates }} modifiersClassNames={{ booked: "[&>button]:line-through opacity-100" }} /> </CardContent> </Card> );}Custom Cell Size
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { addDays } from "date-fns";import { createSignal, Show, splitProps } from "solid-js";import { Calendar, CalendarDayButton, type CalendarDayButtonProps, type CalendarRangeValue,} from "~/components/ui/calendar";import { Card, CardContent } from "~/components/ui/card";
const PriceDayButton = (props: CalendarDayButtonProps) => { const [local, others] = splitProps(props, ["children", "day", "modifiers"]); const isWeekend = () => local.day.date.getDay() === 0 || local.day.date.getDay() === 6;
return ( <CalendarDayButton day={local.day} modifiers={local.modifiers} {...others}> {local.children} <Show when={!local.modifiers.outside}> <span>{isWeekend() ? "$120" : "$100"}</span> </Show> </CalendarDayButton> );};
export default function CalendarCustomDays() { const start = new Date(new Date().getFullYear(), 11, 8); const [range, setRange] = createSignal<CalendarRangeValue>({ from: start, to: addDays(start, 10), });
return ( <Card class="mx-auto w-fit p-0"> <CardContent class="p-0"> <Calendar mode="range" defaultMonth={range().from} selected={range()} onSelect={setRange} captionLayout="dropdown" class="[--cell-size:--spacing(10)] md:[--cell-size:--spacing(12)]" formatters={{ formatMonthDropdown: (date) => date.toLocaleString("default", { month: "long" }), }} components={{ DayButton: PriceDayButton }} /> </CardContent> </Card> );}Customize calendar cells with the --cell-size CSS variable:
<Calendar mode="single" selected={date()} onSelect={setDate} class="rounded-lg border [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"/>Or use fixed values:
<Calendar mode="single" selected={date()} onSelect={setDate} class="rounded-lg border [--cell-size:2.75rem] md:[--cell-size:3rem]"/>Week Numbers
Use showWeekNumber to show ISO week numbers.
| Su | Mo | Tu | We | Th | Fr | Sa | |
|---|---|---|---|---|---|---|---|
1 | |||||||
2 | |||||||
3 | |||||||
4 | |||||||
5 |
import { createSignal } from "solid-js";import { Calendar } from "~/components/ui/calendar";import { Card, CardContent } from "~/components/ui/card";
export default function CalendarWeekNumbers() { const initialDate = new Date(new Date().getFullYear(), 0, 12); const [date, setDate] = createSignal(initialDate);
return ( <Card class="mx-auto w-fit p-0"> <CardContent class="p-0"> <Calendar mode="single" defaultMonth={date()} selected={date()} onSelect={setDate} showWeekNumber /> </CardContent> </Card> );}API Reference
Calendar
The Calendar wrapper follows the Corvu Calendar API reference and adds the DayPicker-compatible props shown below.
For primitive props, keyboard behavior, and accessibility details, see the Corvu Calendar API reference.