Docs
Calendar
Calendar
A calendar component that allows users to select dates.
<script lang="ts">
import { getLocalTimeZone, today } from "@internationalized/date";
import { Calendar } from "$lib/components/ui/calendar/index.js";
let value = $state(today(getLocalTimeZone()));
</script>
<Calendar type="single" bind:value class="rounded-md border shadow" />
<script lang="ts">
import { getLocalTimeZone, today } from "@internationalized/date";
import { Calendar } from "$lib/components/ui/calendar/index.js";
let value = today(getLocalTimeZone());
</script>
<Calendar type="single" bind:value class="rounded-md border" />
About
The <Calendar />
component is built on top of the Bits Calendar component, which uses the @internationalized/date package to handle dates.
If you're looking for a range calendar, check out the Range Calendar component.
Installation
npx shadcn-svelte@next add calendar
Install bits-ui
and @internationalized/date
:
npm i bits-ui @internationalized/date -D
Copy and paste the component source files linked at the top of this page into your project.
Date Picker
You can use the <Calendar />
component to build a date picker. See the Date Picker page for more information.
Examples
Form
<script lang="ts">
import CalendarIcon from "lucide-svelte/icons/calendar";
import {
DateFormatter,
type DateValue,
getLocalTimeZone
} from "@internationalized/date";
import { cn } from "$lib/utils.js";
import { Button } from "$lib/components/ui/button/index.js";
import { Calendar } from "$lib/components/ui/calendar/index.js";
import * as Popover from "$lib/components/ui/popover/index.js";
const df = new DateFormatter("en-US", {
dateStyle: "long"
});
let value: DateValue | undefined = undefined;
</script>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button
variant="outline"
class={cn(
"w-[240px] justify-start text-left font-normal",
!value && "text-muted-foreground"
)}
{...props}
>
<CalendarIcon />
{value ? df.format(value.toDate(getLocalTimeZone())) : "Pick a date"}
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto p-0" align="start">
<Calendar type="single" bind:value />
</Popover.Content>
</Popover.Root>
<script lang="ts">
import CalendarIcon from "lucide-svelte/icons/calendar";
import {
DateFormatter,
type DateValue,
getLocalTimeZone
} from "@internationalized/date";
import { cn } from "$lib/utils.js";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { Calendar } from "$lib/components/ui/calendar/index.js";
import * as Popover from "$lib/components/ui/popover/index.js";
const df = new DateFormatter("en-US", {
dateStyle: "long"
});
let value = $state<DateValue | undefined>();
let contentRef = $state<HTMLElement | null>(null);
</script>
<Popover.Root>
<Popover.Trigger
class={cn(
buttonVariants({
variant: "outline",
class: "w-[280px] justify-start text-left font-normal"
}),
!value && "text-muted-foreground"
)}
>
<CalendarIcon />
{value ? df.format(value.toDate(getLocalTimeZone())) : "Pick a date"}
</Popover.Trigger>
<Popover.Content bind:ref={contentRef} class="w-auto p-0">
<Calendar type="single" bind:value />
</Popover.Content>
</Popover.Root>
Advanced Customization
The <Calendar />
component can be combined with other components to create a more complex calendar.
By default, we export the combined Calendar component as
Calendar
as there are quite a few pieces that need to be combined to create it. We're modifying that component in the examples below.Month & Year Selects
Here's an example of how you could create a calendar with month and year select dropdowns instead of the previous and next buttons.
<script lang="ts">
import {
Calendar as CalendarPrimitive,
type WithoutChildrenOrChild
} from "bits-ui";
import { DateFormatter, getLocalTimeZone } from "@internationalized/date";
import * as Calendar from "$lib/components/ui/calendar/index.js";
import * as Select from "$lib/components/ui/select/index.js";
import { cn } from "$lib/utils.js";
let {
value = $bindable(),
placeholder = $bindable(),
weekdayFormat = "short",
class: className,
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> = $props();
const monthOptions = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
].map((month, i) => ({ value: i + 1, label: month }));
const monthFmt = new DateFormatter("en-US", {
month: "long"
});
const yearOptions = Array.from({ length: 100 }, (_, i) => ({
label: String(new Date().getFullYear() - i),
value: new Date().getFullYear() - i
}));
const defaultYear = $derived(
placeholder
? { value: placeholder.year, label: String(placeholder.year) }
: undefined
);
const defaultMonth = $derived(
placeholder
? {
value: placeholder.month,
label: monthFmt.format(placeholder.toDate(getLocalTimeZone()))
}
: undefined
);
const monthLabel = $derived(
monthOptions.find((m) => m.value === defaultMonth?.value)?.label ??
"Select a month"
);
</script>
<CalendarPrimitive.Root
bind:value={value as never}
bind:placeholder
{weekdayFormat}
class={cn("rounded-md border p-3", className)}
{...restProps}
>
{#snippet children({ months, weekdays })}
<Calendar.Header>
<Calendar.Heading class="flex w-full items-center justify-between gap-2">
<Select.Root
type="single"
value={`${defaultMonth?.value}`}
onValueChange={(v) => {
if (!v || !placeholder) return;
if (v === `${placeholder?.month}`) return;
placeholder = placeholder.set({ month: Number.parseInt(v) });
}}
>
<Select.Trigger aria-label="Select month" class="w-[60%]">
{monthLabel}
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each monthOptions as { value, label }}
<Select.Item value={`${value}`} {label} />
{/each}
</Select.Content>
</Select.Root>
<Select.Root
type="single"
value={`${defaultYear?.value}`}
onValueChange={(v) => {
if (!v || !placeholder) return;
if (v === `${placeholder?.year}`) return;
placeholder = placeholder.set({ year: Number.parseInt(v) });
}}
>
<Select.Trigger aria-label="Select year" class="w-[40%]">
{defaultYear?.label ?? "Select year"}
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each yearOptions as { value, label }}
<Select.Item value={`${value}`} {label} />
{/each}
</Select.Content>
</Select.Root>
</Calendar.Heading>
</Calendar.Header>
<Calendar.Months>
{#each months as month}
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow class="flex">
{#each weekdays as weekday}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates}
<Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date}
<Calendar.Cell {date} month={month.value}>
<Calendar.Day />
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
{/each}
</Calendar.Months>
{/snippet}
</CalendarPrimitive.Root>
<script lang="ts">
import {
Calendar as CalendarPrimitive,
type CalendarRootProps,
type WithoutChildrenOrChild
} from "bits-ui";
import { DateFormatter, getLocalTimeZone } from "@internationalized/date";
import * as Calendar from "$lib/components/ui/calendar/index.js";
import * as Select from "$lib/components/ui/select/index.js";
import { cn } from "$lib/utils.js";
let {
value = $bindable(),
placeholder = $bindable(),
weekdayFormat,
class: className,
...restProps
}: WithoutChildrenOrChild<CalendarRootProps> = $props();
const monthOptions = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
].map((month, i) => ({ value: String(i + 1), label: month }));
const monthFmt = new DateFormatter("en-US", {
month: "long"
});
const yearOptions = Array.from({ length: 100 }, (_, i) => ({
label: String(new Date().getFullYear() - i),
value: String(new Date().getFullYear() - i)
}));
const defaultYear = $derived(
placeholder
? { value: String(placeholder.year), label: String(placeholder.year) }
: undefined
);
const defaultMonth = $derived(
placeholder
? {
value: String(placeholder.month),
label: monthFmt.format(placeholder.toDate(getLocalTimeZone()))
}
: undefined
);
const monthLabel = $derived(
monthOptions.find((m) => m.value === defaultMonth?.value)?.label ??
"Select a month"
);
</script>
<CalendarPrimitive.Root
{weekdayFormat}
class={cn("rounded-md border p-3", className)}
bind:value={value as never}
bind:placeholder
{...restProps}
>
{#snippet children({ months, weekdays })}
<Calendar.Header>
<Calendar.Heading class="flex w-full items-center justify-between gap-2">
<Select.Root
type="single"
value={defaultMonth?.value}
onValueChange={(v) => {
if (!placeholder) return;
if (v === `${placeholder.month}`) return;
placeholder = placeholder.set({ month: Number.parseInt(v) });
}}
>
<Select.Trigger aria-label="Select month" class="w-[60%]">
{monthLabel}
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each monthOptions as { value, label }}
<Select.Item {value} {label} />
{/each}
</Select.Content>
</Select.Root>
<Select.Root
type="single"
value={defaultYear?.value}
onValueChange={(v) => {
if (!v || !placeholder) return;
if (v === `${placeholder?.year}`) return;
placeholder = placeholder.set({ year: Number.parseInt(v) });
}}
>
<Select.Trigger aria-label="Select year" class="w-[40%]">
{defaultYear?.label ?? "Select year"}
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each yearOptions as { value, label }}
<Select.Item {value} {label} />
{/each}
</Select.Content>
</Select.Root>
</Calendar.Heading>
</Calendar.Header>
<Calendar.Months>
{#each months as month}
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow class="flex">
{#each weekdays as weekday}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates}
<Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date}
<Calendar.Cell {date} month={month.value}>
<Calendar.Day />
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
{/each}
</Calendar.Months>
{/snippet}
</CalendarPrimitive.Root>
On This Page