Zaidan

Command Palette

Search for a command to run...

GitHub

Calendar

A date field component that allows users to enter and edit dates.

Installation

CLI

Manual

Install the following dependencies:

Copy and paste the following code into your project.

import Calendar from "@corvu/calendar";
import { getWeek } from "date-fns";
import { ChevronLeft, ChevronRight } from "lucide-solid";
import { type ComponentProps, Index, type JSX, mergeProps, Show, splitProps } from "solid-js";
import { cn } from "~/lib/utils";
import { Button, buttonVariants } from "~/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
type CalendarSingleValue = Date | null;
type CalendarMultipleValue = Date[];
type CalendarRangeValue = { from: Date | null; to: Date | null };
/**
* Props passed to the customCell render function
*/
type CustomCellProps = {
/** The date of the cell */
date: Date;
/** Whether the date is outside the current month */
isOutsideMonth: boolean;
/** Whether the date is selected */
isSelected: boolean;
/** Whether the date is disabled */
isDisabled: boolean;
/** Whether the date is today */
isToday: boolean;
};
type CalendarBaseProps = Omit<ComponentProps<"div">, "onChange"> & {
/**
* Whether to show days from the previous/next months
* @default true
*/
showOutsideDays?: boolean;
/**
* Whether to always show 6 weeks in the calendar
* @default false
*/
fixedWeeks?: boolean;
/**
* Number of months to display
* @default 1
*/
numberOfMonths?: number;
/**
* Function to disable specific dates (grayed out, not selectable)
*/
disabled?: (date: Date) => boolean;
/**
* Function to mark specific dates as booked (strikethrough styling)
* Booked dates are also disabled but have different visual styling
*/
booked?: (date: Date) => boolean;
/**
* The controlled month to display
*/
month?: Date;
/**
* Callback when the displayed month changes
*/
onMonthChange?: (month: Date) => void;
/**
* The initial month to display (uncontrolled)
*/
defaultMonth?: Date;
/**
* Which day of the week to start on (0 = Sunday, 1 = Monday, etc.)
* @default 1
*/
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
/**
* Whether to show week numbers in the calendar
* @default false
*/
weekNumbers?: boolean;
/**
* Custom render function for day cells
* Allows adding metadata like prices, events, etc.
*/
customCell?: (props: CustomCellProps) => JSX.Element;
/**
* Enable month/year selection via dropdown selects
* @default false
*/
monthYearSelection?: boolean;
/**
* Start year for the year dropdown (only used when monthYearSelection is true)
* @default current year - 100
*/
startYear?: number;
/**
* End year for the year dropdown (only used when monthYearSelection is true)
* @default current year + 10
*/
endYear?: number;
};
type CalendarSingleProps = CalendarBaseProps & {
mode?: "single";
value?: CalendarSingleValue;
onValueChange?: (value: CalendarSingleValue) => void;
defaultValue?: CalendarSingleValue;
};
type CalendarMultipleProps = CalendarBaseProps & {
mode: "multiple";
value?: CalendarMultipleValue;
onValueChange?: (value: CalendarMultipleValue) => void;
defaultValue?: CalendarMultipleValue;
};
type CalendarRangeProps = CalendarBaseProps & {
mode: "range";
value?: CalendarRangeValue;
onValueChange?: (value: CalendarRangeValue) => void;
defaultValue?: CalendarRangeValue;
};
type CalendarProps = CalendarSingleProps | CalendarMultipleProps | CalendarRangeProps;
const MONTHS = [
{ label: "Jan", value: 0 },
{ label: "Feb", value: 1 },
{ label: "Mar", value: 2 },
{ label: "Apr", value: 3 },
{ label: "May", value: 4 },
{ label: "Jun", value: 5 },
{ label: "Jul", value: 6 },
{ label: "Aug", value: 7 },
{ label: "Sep", value: 8 },
{ label: "Oct", value: 9 },
{ label: "Nov", value: 10 },
{ label: "Dec", value: 11 },
];
const CalendarComponent = (props: CalendarProps) => {
const currentYear = new Date().getFullYear();
const mergedProps = mergeProps(
{
mode: "single" as const,
showOutsideDays: true,
fixedWeeks: false,
numberOfMonths: 1,
weekStartsOn: 1 as const,
weekNumbers: false,
monthYearSelection: false,
startYear: currentYear - 100,
endYear: currentYear + 10,
},
props,
);
const [local, others] = splitProps(mergedProps, [
"class",
"mode",
"value",
"onValueChange",
"defaultValue",
"showOutsideDays",
"fixedWeeks",
"numberOfMonths",
"disabled",
"booked",
"month",
"onMonthChange",
"defaultMonth",
"weekStartsOn",
"weekNumbers",
"customCell",
"monthYearSelection",
"startYear",
"endYear",
]);
const formatMonth = (date: Date) => {
return date.toLocaleString("default", { month: "long", year: "numeric" });
};
const formatWeekday = (date: Date) => {
return date.toLocaleString("default", { weekday: "short" }).slice(0, 2);
};
// Generate years array for dropdown
const years = () =>
Array.from({ length: local.endYear - local.startYear + 1 }, (_, i) => {
const year = local.startYear + i;
return { label: year.toString(), value: year };
});
return (
// @ts-expect-error - Calendar component is not typed correctly
<Calendar
mode={local.mode}
value={local.value as Date | null}
onValueChange={local.onValueChange as (value: Date | null) => void}
initialValue={local.defaultValue as Date | null}
month={local.month}
onMonthChange={local.onMonthChange}
initialMonth={local.defaultMonth}
numberOfMonths={local.numberOfMonths}
fixedWeeks={local.fixedWeeks}
disableOutsideDays={!local.showOutsideDays}
disabled={(date: Date) => local.disabled?.(date) || local.booked?.(date) || false}
startOfWeek={local.weekStartsOn}
>
{/* @ts-expect-error - Calendar component is not typed correctly */}
{(calendarProps) => (
<div
data-slot="calendar"
class={cn(
"group/calendar z-calendar w-fit bg-popover p-3",
"in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
local.class,
)}
{...others}
>
<div class="flex flex-col gap-4 md:flex-row">
<Index each={calendarProps.months}>
{(monthData, index) => (
<div data-slot="calendar-month" class="flex w-full flex-col gap-4">
{/* Navigation and Header */}
<nav
data-slot="calendar-header"
class="flex h-(--cell-size) w-full items-center justify-between gap-1"
>
<Calendar.Nav
action="prev-month"
as={Button}
variant="ghost"
class={cn(
buttonVariants({ variant: "ghost" }),
"size-(--cell-size) select-none p-0",
)}
>
<ChevronLeft class="size-4" />
<span class="sr-only">Previous month</span>
</Calendar.Nav>
{/* Month/Year Selection or Label */}
<Show
when={local.monthYearSelection}
fallback={
<h2
class="flex-1 select-none text-center font-medium text-sm"
data-slot="calendar-label"
id={calendarProps.labelIds[index]?.()}
>
{formatMonth(monthData().month)}
</h2>
}
>
<div class="flex flex-1 items-center justify-center gap-2">
<Select<(typeof MONTHS)[number]>
options={MONTHS}
optionValue="value"
optionTextValue="label"
value={MONTHS.find((m) => m.value === monthData().month.getMonth())}
onChange={(selectedMonth) => {
if (selectedMonth) {
const newDate = new Date(monthData().month);
newDate.setMonth(selectedMonth.value);
calendarProps.setMonth(newDate);
}
}}
itemComponent={(itemProps) => (
<SelectItem item={itemProps.item}>
{itemProps.item.rawValue.label}
</SelectItem>
)}
>
<SelectTrigger size="sm">
<SelectValue<(typeof MONTHS)[number]>>
{(state) => state.selectedOption().label}
</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
<Select<{ label: string; value: number }>
options={years()}
optionValue="value"
optionTextValue="label"
value={years().find((y) => y.value === monthData().month.getFullYear())}
onChange={(selectedYear) => {
if (selectedYear) {
const newDate = new Date(monthData().month);
newDate.setFullYear(selectedYear.value);
calendarProps.setMonth(newDate);
}
}}
itemComponent={(itemProps) => (
<SelectItem item={itemProps.item}>
{itemProps.item.rawValue.label}
</SelectItem>
)}
>
<SelectTrigger size="sm">
<SelectValue<{ label: string; value: number }>>
{(state) => state.selectedOption().label}
</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
</div>
</Show>
<Calendar.Nav
action="next-month"
as={Button}
variant="ghost"
class={cn(
buttonVariants({ variant: "ghost" }),
"size-(--cell-size) select-none p-0",
)}
>
<ChevronRight class="size-4" />
<span class="sr-only">Next month</span>
</Calendar.Nav>
</nav>
<Calendar.Table index={index} class="w-full border-collapse">
<thead data-slot="calendar-weekdays">
<tr class="flex">
{/* Week number header */}
<Show when={local.weekNumbers}>
<th
data-slot="calendar-week-number-header"
class="w-8 flex-none select-none rounded-(--cell-radius) font-normal text-[0.8rem] text-muted-foreground"
>
#
</th>
</Show>
<Index each={calendarProps.weekdays}>
{(weekday) => (
<Calendar.HeadCell
data-slot="calendar-weekday"
class="flex-1 select-none rounded-(--cell-radius) font-normal text-[0.8rem] text-muted-foreground"
>
{formatWeekday(weekday())}
</Calendar.HeadCell>
)}
</Index>
</tr>
</thead>
<tbody data-slot="calendar-weeks">
<Index each={monthData().weeks}>
{(week) => (
<tr data-slot="calendar-week" class="mt-2 flex w-full">
{/* Week number cell */}
<Show when={local.weekNumbers}>
<td
data-slot="calendar-week-number"
class="flex w-8 flex-none select-none items-center justify-center font-normal text-[0.75rem] text-muted-foreground"
>
{getWeekNumber(week())}
</td>
</Show>
<Index each={week()}>
{(day) => (
<Show when={day()} fallback={<td class="flex-1 p-0" />}>
{(d) => (
<CalendarDay
day={d()}
month={monthData().month}
mode={local.mode}
value={calendarProps.value}
disabled={local.disabled}
booked={local.booked}
customCell={local.customCell}
/>
)}
</Show>
)}
</Index>
</tr>
)}
</Index>
</tbody>
</Calendar.Table>
</div>
)}
</Index>
</div>
</div>
)}
</Calendar>
);
};
/**
* Get the ISO week number for a week (using the first non-null day)
*/
const getWeekNumber = (week: (Date | null)[]): number => {
const firstDay = week.find((d) => d !== null);
if (!firstDay) return 0;
return getWeek(firstDay, { weekStartsOn: 1 });
};
type CalendarDayProps = {
day: Date;
month: Date;
mode: "single" | "multiple" | "range";
value: Date | null | Date[] | { from: Date | null; to: Date | null };
disabled?: (date: Date) => boolean;
booked?: (date: Date) => boolean;
customCell?: (props: CustomCellProps) => JSX.Element;
};
const CalendarDay = (props: CalendarDayProps) => {
const isOutsideMonth = () => props.day.getMonth() !== props.month.getMonth();
const isToday = () => isSameDay(props.day, new Date());
const isDisabled = () => props.disabled?.(props.day) ?? false;
const isBooked = () => props.booked?.(props.day) ?? false;
const isSelected = () => {
const value = props.value;
if (value === null || value === undefined) return false;
if (value instanceof Date) {
return isSameDay(value, props.day);
}
if (Array.isArray(value)) {
return value.some((d) => isSameDay(d, props.day));
}
if (typeof value === "object" && "from" in value) {
const { from, to } = value as CalendarRangeValue;
if (from && isSameDay(from, props.day)) return true;
if (to && isSameDay(to, props.day)) return true;
return false;
}
return false;
};
const isRangeStart = () => {
const value = props.value;
if (!value || typeof value !== "object" || !("from" in value)) return false;
const { from } = value as CalendarRangeValue;
return from && isSameDay(from, props.day);
};
const isRangeEnd = () => {
const value = props.value;
if (!value || typeof value !== "object" || !("from" in value)) return false;
const { to } = value as CalendarRangeValue;
return to && isSameDay(to, props.day);
};
const isInRange = () => {
const value = props.value;
if (!value || typeof value !== "object" || !("from" in value)) return false;
const { from, to } = value as CalendarRangeValue;
if (!from || !to) return false;
return props.day > from && props.day < to;
};
const isSingleSelected = () => isSelected() && !isRangeStart() && !isRangeEnd() && !isInRange();
return (
<Calendar.Cell
data-slot="calendar-day"
class={cn(
"group/day relative aspect-square h-full w-full flex-1 select-none rounded-(--cell-radius) p-0 text-center",
"[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
"[&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
isRangeStart() &&
"isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted",
isRangeEnd() &&
"isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted",
isInRange() && "rounded-none",
)}
data-selected={isSelected() || undefined}
data-outside={isOutsideMonth() || undefined}
>
<Calendar.CellTrigger
day={props.day}
month={props.month}
data-slot="calendar-day-button"
class={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-0.5 border-0 font-normal leading-none",
// Focus states
"group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50",
// Today styling
"data-[today=true]:data-[selected=true]:rounded-none data-[today=true]:rounded-(--cell-radius) data-[today=true]:bg-muted data-[today=true]:text-foreground",
// Single selection
isSingleSelected() && "bg-primary text-primary-foreground",
// Range selection
isRangeStart() &&
"rounded-(--cell-radius) rounded-l-(--cell-radius) bg-primary text-primary-foreground",
isRangeEnd() &&
"rounded-(--cell-radius) rounded-r-(--cell-radius) bg-primary text-primary-foreground",
isInRange() && "rounded-none bg-muted text-foreground",
// Outside month
"data-[outside=true]:text-muted-foreground data-[outside=true]:aria-selected:text-muted-foreground",
// Disabled (not selectable, grayed out)
"data-disabled:text-muted-foreground data-disabled:opacity-50",
// Booked (strikethrough styling)
isBooked() && "line-through",
// Custom cell styling - add padding if custom cell is provided
props.customCell && "h-auto min-h-(--cell-size) py-1",
)}
data-outside={isOutsideMonth() || undefined}
>
<span>{props.day.getDate()}</span>
<Show when={props.customCell}>
{(renderCustomCell) =>
renderCustomCell()({
date: props.day,
isOutsideMonth: isOutsideMonth(),
isSelected: isSelected(),
isDisabled: isDisabled(),
isToday: isToday(),
})
}
</Show>
</Calendar.CellTrigger>
</Calendar.Cell>
);
};
// Helper function
const isSameDay = (a: Date, b: Date): boolean => {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
};
export { CalendarComponent as Calendar, type CalendarProps, type CustomCellProps };

About

The Calendar component is built on top of Corvu Calendar. It provides a fully accessible date selection component with keyboard navigation and support for single, multiple, and range selection modes.

Props

The Calendar component accepts the following props:

PropTypeDefaultDescription
mode"single" | "multiple" | "range""single"The selection mode
valueDate | null | Date[] | { from: Date | null, to: Date | null }-The controlled value
onValueChange(value) => void-Handler called when value changes
defaultValueSame as value-The default uncontrolled value
monthDate-The controlled month to display
onMonthChange(month: Date) => void-Handler called when month changes
defaultMonthDatenew Date()The initial month to display
showOutsideDaysbooleantrueWhether to show days from adjacent months
fixedWeeksbooleanfalseWhether to always show 6 weeks
numberOfMonthsnumber1Number of months to display
disabled(date: Date) => boolean-Function to disable specific dates
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 61Which day of the week to start on (0 = Sunday)
weekNumbersbooleanfalseWhether to show ISO week numbers
customCell(props: CustomCellProps) => JSX.Element-Custom render function for day cells
monthYearSelectionbooleanfalseEnable month/year dropdown selection
startYearnumbercurrentYear - 100Start year for year dropdown
endYearnumbercurrentYear + 10End year for year dropdown
classstring-Additional CSS classes

CustomCellProps

The customCell render function receives the following props:

PropTypeDescription
dateDateThe date of the cell
isOutsideMonthbooleanWhether the date is outside the current month
isSelectedbooleanWhether the date is selected
isDisabledbooleanWhether the date is disabled
isTodaybooleanWhether the date is today

Date Picker

You can use the <Calendar> component to build a date picker by combining it with <Popover> and <Button> components. See the Date Picker examples below.

Custom Cell Size

You can customize the size of calendar cells using the --cell-size CSS variable. You can also make it responsive by using breakpoint-specific values:

<Calendar
mode="single"
value={date()}
onValueChange={setDate}
class="rounded-lg border [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/>

Week Numbers

Display ISO week numbers alongside the calendar using the weekNumbers prop:

<Calendar
mode="single"
value={date()}
onValueChange={setDate}
weekNumbers
/>

Custom Cells

Use the customCell prop to add custom content to each day cell. This is useful for displaying metadata like prices, events, or availability indicators:

import { Show } from "solid-js";
import { Calendar, type CustomCellProps } from "~/components/ui/calendar";
const renderPriceCell = (props: CustomCellProps) => {
const price = getPriceForDate(props.date);
return (
<Show when={price !== null && !props.isOutsideMonth}>
<span class={`text-[0.65rem] ${props.isSelected ? "text-primary-foreground/80" : "text-muted-foreground"}`}>
${price}
</span>
</Show>
);
};
<Calendar
mode="single"
value={date()}
onValueChange={setDate}
customCell={renderPriceCell}
class="[--cell-size:--spacing(11)]"
/>

Month/Year Selection

Enable dropdown selection for month and year using the monthYearSelection prop:

<Calendar
mode="single"
value={date()}
onValueChange={setDate}
monthYearSelection
startYear={1990}
endYear={2030}
/>

Combining with Time Inputs

You can combine the calendar with time inputs to create a date-time picker:

import { createSignal } from "solid-js";
import { Calendar } from "~/components/ui/calendar";
import { Input } from "~/components/ui/input";
const [date, setDate] = createSignal<Date | null>(null);
const [time, setTime] = createSignal("10:30");
<div>
<Calendar mode="single" value={date()} onValueChange={setDate} />
<Input
type="time"
value={time()}
onInput={(e) => setTime(e.currentTarget.value)}
/>
</div>

Styling Disabled Dates

You can style disabled dates differently using the disabled prop and CSS selectors:

import { Calendar } from "~/components/ui/calendar";
const bookedDates = [
new Date(2024, 0, 15),
new Date(2024, 0, 20),
];
const isBooked = (day: Date) => {
return bookedDates.some(booked =>
booked.toDateString() === day.toDateString()
);
};
<Calendar
mode="single"
disabled={isBooked}
class="**:data-[disabled=true]:line-through **:data-[disabled=true]:opacity-30"
/>

Examples

Here are the source code of all the examples from the preview page:

Single

import { createSignal } from "solid-js";
import { Calendar } from "~/components/ui/calendar";
import { Card, CardContent } from "~/components/ui/card";
function CalendarSingle() {
const [date, setDate] = createSignal<Date | null>(
new Date(new Date().getFullYear(), new Date().getMonth(), 12),
);
return (
<Example title="Single">
<Card class="mx-auto w-fit p-0">
<CardContent class="p-0">
<Calendar mode="single" monthYearSelection value={date()} onValueChange={setDate} />
</CardContent>
</Card>
</Example>
);
}

Multiple

import { Calendar } from "~/components/ui/calendar";
import { Card, CardContent } from "~/components/ui/card";
function CalendarMultiple() {
return (
<Example title="Multiple">
<Card class="mx-auto w-fit p-0">
<CardContent class="p-0">
<Calendar mode="multiple" />
</CardContent>
</Card>
</Example>
);
}

Range

import { createSignal } from "solid-js";
import { Calendar } from "~/components/ui/calendar";
import { Card, CardContent } from "~/components/ui/card";
function CalendarRange() {
const addDays = (date: Date, days: number) => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
const [dateRange, setDateRange] = createSignal<{ from: Date | null; to: Date | null }>({
from: new Date(new Date().getFullYear(), 0, 12),
to: addDays(new Date(new Date().getFullYear(), 0, 12), 30),
});
return (
<Example title="Range" containerClass="lg:col-span-full 2xl:col-span-full" class="p-12">
<Card class="mx-auto w-fit p-0">
<CardContent class="p-0">
<Calendar
mode="range"
defaultMonth={dateRange().from ?? undefined}
value={dateRange()}
onValueChange={setDateRange}
numberOfMonths={2}
disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
/>
</CardContent>
</Card>
</Example>
);
}

Range Multiple Months

import { createSignal } from "solid-js";
import { Calendar } from "~/components/ui/calendar";
import { Card, CardContent } from "~/components/ui/card";
function CalendarRangeMultipleMonths() {
const addDays = (date: Date, days: number) => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
const [range, setRange] = createSignal<{ from: Date | null; to: Date | null }>({
from: new Date(new Date().getFullYear(), 3, 12),
to: addDays(new Date(new Date().getFullYear(), 3, 12), 60),
});
return (
<Example
title="Range Multiple Months"
containerClass="lg:col-span-full 2xl:col-span-full"
class="p-12"
>
<Card class="mx-auto w-fit p-0">
<CardContent class="p-0">
<Calendar
mode="range"
defaultMonth={range().from ?? undefined}
value={range()}
onValueChange={setRange}
numberOfMonths={3}
fixedWeeks
/>
</CardContent>
</Card>
</Example>
);
}

Week Numbers

import { createSignal } from "solid-js";
import { Calendar } from "~/components/ui/calendar";
import { Card, CardContent } from "~/components/ui/card";
function CalendarWeekNumbers() {
const [date, setDate] = createSignal<Date | null>(
new Date(new Date().getFullYear(), new Date().getMonth(), 15),
);
return (
<Example title="Week Numbers">
<Card class="mx-auto w-fit p-0">
<CardContent class="p-0">
<Calendar mode="single" value={date()} onValueChange={setDate} weekNumbers />
</CardContent>
</Card>
</Example>
);
}

Custom Cell (Pricing)

import { createSignal, Show } from "solid-js";
import { Calendar, type CustomCellProps } from "~/components/ui/calendar";
import { Card, CardContent } from "~/components/ui/card";
function CalendarCustomCell() {
const [range, setRange] = createSignal<{ from: Date | null; to: Date | null }>({
from: new Date(new Date().getFullYear(), 0, 8),
to: addDays(new Date(new Date().getFullYear(), 0, 8), 10),
});
const renderPriceCell = (props: CustomCellProps) => {
const isWeekend = () => props.date.getDay() === 0 || props.date.getDay() === 6;
return (
<Show when={!props.isOutsideMonth}>
<span
class={`text-[0.65rem] ${props.isSelected ? "text-primary-foreground/80" : "text-muted-foreground"}`}
>
${isWeekend() ? "100" : "80"}
</span>
</Show>
);
};
return (
<Example title="Custom Cell (Pricing)">
<Card class="mx-auto w-fit p-0">
<CardContent class="p-0">
<Calendar
mode="range"
value={range()}
onValueChange={setRange}
customCell={renderPriceCell}
class="[--cell-size:--spacing(11)]"
/>
</CardContent>
</Card>
</Example>
);
}

With Time

import { createSignal } from "solid-js";
import { Calendar } from "~/components/ui/calendar";
import { Card, CardContent } from "~/components/ui/card";
import { Field, FieldLabel } from "~/components/ui/field";
import { Input } from "~/components/ui/input";
function CalendarWithTime() {
const [date, setDate] = createSignal<Date | null>(
new Date(new Date().getFullYear(), new Date().getMonth(), 15),
);
const [startTime, setStartTime] = createSignal("10:30");
const [endTime, setEndTime] = createSignal("12:30");
return (
<Example title="With Time">
<Card class="mx-auto w-fit max-w-sm" size="sm">
<CardContent>
<Calendar mode="single" value={date()} onValueChange={setDate} fixedWeeks class="p-0" />
<div class="mt-4 flex gap-2">
<Field class="flex-1">
<FieldLabel for="start-time">Start Time</FieldLabel>
<Input
id="start-time"
type="time"
value={startTime()}
onInput={(e) => setStartTime(e.currentTarget.value)}
/>
</Field>
<Field class="flex-1">
<FieldLabel for="end-time">End Time</FieldLabel>
<Input
id="end-time"
type="time"
value={endTime()}
onInput={(e) => setEndTime(e.currentTarget.value)}
/>
</Field>
</div>
</CardContent>
</Card>
</Example>
);
}

Booked Dates

import { createSignal } from "solid-js";
import { Calendar } from "~/components/ui/calendar";
import { Card, CardContent } from "~/components/ui/card";
function CalendarBookedDates() {
const [date, setDate] = createSignal<Date | null>(null);
// Dates that are "booked" (shown as disabled with different styling)
const bookedDates = [
new Date(new Date().getFullYear(), new Date().getMonth(), 8),
new Date(new Date().getFullYear(), new Date().getMonth(), 9),
new Date(new Date().getFullYear(), new Date().getMonth(), 10),
new Date(new Date().getFullYear(), new Date().getMonth(), 15),
new Date(new Date().getFullYear(), new Date().getMonth(), 16),
new Date(new Date().getFullYear(), new Date().getMonth(), 20),
new Date(new Date().getFullYear(), new Date().getMonth(), 25),
];
const isSameDay = (a: Date, b: Date): boolean => {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
};
const isBooked = (day: Date) => {
return bookedDates.some((bookedDate) => isSameDay(bookedDate, day));
};
return (
<Example title="Booked Dates">
<Card class="mx-auto w-fit p-0">
<CardContent class="p-0">
<Calendar mode="single" value={date()} onValueChange={setDate} booked={isBooked} />
</CardContent>
</Card>
</Example>
);
}

With Presets

import { createSignal } from "solid-js";
import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar";
import { Card, CardContent, CardFooter } from "~/components/ui/card";
function CalendarWithPresets() {
const addDays = (date: Date, days: number) => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
const [date, setDate] = createSignal<Date | null>(new Date(new Date().getFullYear(), 1, 12));
const [currentMonth, setCurrentMonth] = createSignal<Date>(
new Date(new Date().getFullYear(), new Date().getMonth(), 1),
);
return (
<Example title="With Presets">
<Card class="mx-auto w-fit max-w-[300px]" size="sm">
<CardContent>
<Calendar
mode="single"
value={date()}
onValueChange={setDate}
month={currentMonth()}
onMonthChange={setCurrentMonth}
fixedWeeks
class="p-0 [--cell-size:--spacing(9.5)]"
/>
</CardContent>
<CardFooter class="flex flex-wrap gap-2 border-t">
{[
{ 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 },
].map((preset) => (
<Button
variant="outline"
size="sm"
class="flex-1"
onClick={() => {
const newDate = addDays(new Date(), preset.value);
setDate(newDate);
setCurrentMonth(new Date(newDate.getFullYear(), newDate.getMonth(), 1));
}}
>
{preset.label}
</Button>
))}
</CardFooter>
</Card>
</Example>
);
}

Date Picker Simple

import { CalendarIcon } from "lucide-solid";
import { createSignal, Show } from "solid-js";
import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar";
import { Field, FieldLabel } from "~/components/ui/field";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
function DatePickerSimple() {
const [date, setDate] = createSignal<Date | null>(null);
const formatDate = (date: Date) => {
return date.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
};
return (
<Example title="Date Picker Simple">
<Field class="mx-auto w-72">
<FieldLabel for="date-picker-simple">Date</FieldLabel>
<Popover>
<PopoverTrigger
as={Button}
variant="outline"
id="date-picker-simple"
class="justify-start px-2.5 font-normal"
>
<CalendarIcon data-icon="inline-start" />
<Show when={date()} fallback={<span>Pick a date</span>} keyed>
{(d) => formatDate(d)}
</Show>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar mode="single" value={date()} onValueChange={setDate} />
</PopoverContent>
</Popover>
</Field>
</Example>
);
}

Date Picker with Dropdowns

import { CalendarIcon } from "lucide-solid";
import { createSignal, Show } from "solid-js";
import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar";
import { Field, FieldLabel } from "~/components/ui/field";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
function DatePickerWithDropdowns() {
const [date, setDate] = createSignal<Date | null>(null);
const formatDate = (date: Date) => {
return date.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
};
return (
<Example title="Date Picker with Dropdowns">
<Field class="mx-auto w-72">
<FieldLabel for="date-picker-dropdowns">Date</FieldLabel>
<Popover>
<PopoverTrigger
as={Button}
variant="outline"
id="date-picker-dropdowns"
class="justify-start px-2.5 font-normal"
>
<CalendarIcon data-icon="inline-start" />
<Show when={date()} fallback={<span>Pick a date</span>} keyed>
{(d) => formatDate(d)}
</Show>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar mode="single" monthYearSelection value={date()} onValueChange={setDate} />
</PopoverContent>
</Popover>
</Field>
</Example>
);
}

Date Picker Range

import { CalendarIcon } from "lucide-solid";
import { createSignal, Show } from "solid-js";
import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar";
import { Field, FieldLabel } from "~/components/ui/field";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
function DatePickerWithRange() {
const addDays = (date: Date, days: number) => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
const [date, setDate] = createSignal<{ from: Date | null; to: Date | null }>({
from: new Date(new Date().getFullYear(), 0, 20),
to: addDays(new Date(new Date().getFullYear(), 0, 20), 20),
});
const formatDate = (date: Date) => {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
};
return (
<Example title="Date Picker Range">
<Field class="mx-auto w-72">
<FieldLabel for="date-picker-range">Date Picker Range</FieldLabel>
<Popover>
<PopoverTrigger
as={Button}
variant="outline"
id="date-picker-range"
class="justify-start px-2.5 font-normal"
>
<CalendarIcon data-icon="inline-start" />
<Show when={date().from} fallback={<span>Pick a date</span>} keyed>
{(from) => (
<Show when={date().to} fallback={formatDate(from)} keyed>
{(to) => (
<>
{formatDate(from)} - {formatDate(to)}
</>
)}
</Show>
)}
</Show>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar
mode="range"
defaultMonth={date().from ?? undefined}
value={date()}
onValueChange={setDate}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
</Field>
</Example>
);
}