| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
<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-sm"
captionLayout="dropdown"
/> Blocks
We have built a collection of 30+ calendar blocks that you can use to build your own calendar components.
Installation
pnpm dlx shadcn-svelte@latest add calendar Install bits-ui and @internationalized/date:
pnpm add bits-ui @internationalized/date -D Copy and paste the following code into your project.
<script lang="ts">
import type { ComponentProps } from 'svelte';
import type Calendar from './calendar.svelte';
import CalendarMonthSelect from './calendar-month-select.svelte';
import CalendarYearSelect from './calendar-year-select.svelte';
import { DateFormatter, getLocalTimeZone, type DateValue } from '@internationalized/date';
let {
captionLayout,
months,
monthFormat,
years,
yearFormat,
month,
locale,
placeholder = $bindable(),
monthIndex = 0
}: {
captionLayout: ComponentProps<typeof Calendar>['captionLayout'];
months: ComponentProps<typeof CalendarMonthSelect>['months'];
monthFormat: ComponentProps<typeof CalendarMonthSelect>['monthFormat'];
years: ComponentProps<typeof CalendarYearSelect>['years'];
yearFormat: ComponentProps<typeof CalendarYearSelect>['yearFormat'];
month: DateValue;
placeholder: DateValue | undefined;
locale: string;
monthIndex: number;
} = $props();
function formatYear(date: DateValue) {
const dateObj = date.toDate(getLocalTimeZone());
if (typeof yearFormat === 'function') return yearFormat(dateObj.getFullYear());
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
}
function formatMonth(date: DateValue) {
const dateObj = date.toDate(getLocalTimeZone());
if (typeof monthFormat === 'function') return monthFormat(dateObj.getMonth() + 1);
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
}
</script>
{#snippet MonthSelect()}
<CalendarMonthSelect
class="font-mono text-xs font-semibold tracking-[0.14em] uppercase"
{months}
{monthFormat}
value={month.month}
onchange={(e) => {
if (!placeholder) return;
const v = Number.parseInt(e.currentTarget.value);
const newPlaceholder = placeholder.set({ month: v });
placeholder = newPlaceholder.subtract({ months: monthIndex });
}}
/>
{/snippet}
{#snippet YearSelect()}
<CalendarYearSelect
class="font-mono text-xs font-semibold tracking-[0.14em] uppercase"
{years}
{yearFormat}
value={month.year}
/>
{/snippet}
{#if captionLayout === 'dropdown'}
{@render MonthSelect()}
{@render YearSelect()}
{:else if captionLayout === 'dropdown-months'}
{@render MonthSelect()}
{#if placeholder}
<span class="font-mono text-xs font-semibold tracking-[0.14em] text-zinc-100 uppercase"
>{formatYear(placeholder)}</span
>
{/if}
{:else if captionLayout === 'dropdown-years'}
{#if placeholder}
<span class="font-mono text-xs font-semibold tracking-[0.14em] text-zinc-100 uppercase"
>{formatMonth(placeholder)}</span
>
{/if}
{@render YearSelect()}
{:else}
<span class="font-mono text-xs font-semibold tracking-[0.14em] text-zinc-100 uppercase"
>{formatMonth(month)} {formatYear(month)}</span
>
{/if}
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.CellProps = $props();
</script>
<CalendarPrimitive.Cell
bind:ref
class={cn(
'relative size-(--cell-size) border-r border-b border-zinc-900 p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-s-(--cell-radius) [&:last-child[data-selected]_[data-bits-day]]:rounded-e-(--cell-radius)',
className
)}
{...restProps}
/>
<script lang="ts">
import { cn } from '$UTILS$.js';
import { Calendar as CalendarPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.DayProps = $props();
</script>
<CalendarPrimitive.Day
bind:ref
class={cn(
'flex size-(--cell-size) flex-col items-center justify-center gap-1 rounded-(--cell-radius) border border-transparent p-0 font-mono text-xs leading-none font-medium whitespace-nowrap text-zinc-300 select-none',
'[&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)',
'not-data-selected:hover:border-zinc-700 not-data-selected:hover:bg-zinc-950 not-data-selected:hover:text-white',
'[&[data-today]:not([data-selected])]:border-[#d0e891]/60 [&[data-today]:not([data-selected])]:bg-zinc-950 [&[data-today]:not([data-selected])]:text-[#d0e891] [&[data-today][data-disabled]]:text-zinc-500',
'data-[selected]:border-[#b9d765] data-[selected]:bg-[#b9d765] data-[selected]:text-[#101207] data-[selected]:hover:text-[#101207]',
// Outside months
'[&[data-outside-month]:not([data-selected])]:text-zinc-700 [&[data-outside-month]:not([data-selected])]:hover:text-zinc-400',
// Disabled
'data-[disabled]:pointer-events-none data-[disabled]:text-zinc-600 data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-zinc-600 data-[unavailable]:line-through',
// focus
'focus:relative focus:border-zinc-300 focus:ring-2 focus:ring-zinc-300/30',
// inner spans
'[&>span]:text-xs [&>span]:opacity-70',
className
)}
{...restProps}
/>
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridBodyProps = $props();
</script>
<CalendarPrimitive.GridBody bind:ref class={cn('bg-background', className)} {...restProps} />
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridHeadProps = $props();
</script>
<CalendarPrimitive.GridHead bind:ref class={cn('bg-zinc-950/70', className)} {...restProps} />
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridRowProps = $props();
</script>
<CalendarPrimitive.GridRow
bind:ref
class={cn('flex divide-x divide-zinc-900', className)}
{...restProps}
/>
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridProps = $props();
</script>
<CalendarPrimitive.Grid
bind:ref
class={cn('flex w-full border-collapse flex-col border border-zinc-900 bg-background', className)}
{...restProps}
/>
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeadCellProps = $props();
</script>
<CalendarPrimitive.HeadCell
bind:ref
class={cn(
'w-(--cell-size) py-1 font-mono text-[0.7rem] font-semibold tracking-[0.14em] text-zinc-500 uppercase',
className
)}
{...restProps}
/>
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeaderProps = $props();
</script>
<CalendarPrimitive.Header
bind:ref
class={cn(
'flex h-(--cell-size) w-full items-center justify-center gap-1.5 border-b border-zinc-900 text-sm font-medium',
className
)}
{...restProps}
/>
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeadingProps = $props();
</script>
<CalendarPrimitive.Heading
bind:ref
class={cn(
'px-(--cell-size) font-mono text-xs font-semibold tracking-[0.14em] text-zinc-100 uppercase',
className
)}
{...restProps}
/>
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$UTILS$.js';
import CaretDownIcon from 'phosphor-svelte/lib/CaretDown';
let {
ref = $bindable(null),
class: className,
value,
onchange,
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.MonthSelectProps> = $props();
</script>
<span
class={cn(
'relative flex border border-zinc-800 bg-zinc-950 text-zinc-100 has-focus:border-zinc-300 has-focus:ring-2 has-focus:ring-zinc-300/30',
className
)}
>
<CalendarPrimitive.MonthSelect
bind:ref
class="bg-background dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
{...restProps}
>
{#snippet child({ props, monthItems, selectedMonthItem })}
<select {...props} {value} {onchange}>
{#each monthItems as monthItem (monthItem.value)}
<option
value={monthItem.value}
selected={value !== undefined
? monthItem.value === value
: monthItem.value === selectedMonthItem.value}
>
{monthItem.label}
</option>
{/each}
</select>
<span
class="flex h-(--cell-size) items-center gap-1 ps-2 pe-1 text-sm font-medium text-zinc-100 select-none [&>svg]:size-3.5 [&>svg]:text-zinc-500"
aria-hidden="true"
>
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
<CaretDownIcon class={cn('size-4', className)} />
</span>
{/snippet}
</CalendarPrimitive.MonthSelect>
</span>
<script lang="ts">
import { type WithElementRef, cn } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div {...restProps} bind:this={ref} class={cn('flex w-full flex-col gap-3', className)}>
{@render children?.()}
</div>
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn('relative flex flex-col gap-4 text-zinc-100 md:flex-row', className)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<nav
{...restProps}
bind:this={ref}
class={cn(
'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1 text-zinc-300',
className
)}
>
{@render children?.()}
</nav>
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import CaretRightIcon from 'phosphor-svelte/lib/CaretRight';
import { buttonVariants, type ButtonVariant } from '$UI$/button/index.js';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
children,
variant = 'ghost',
...restProps
}: CalendarPrimitive.NextButtonProps & {
variant?: ButtonVariant;
} = $props();
</script>
{#snippet Fallback()}
<CaretRightIcon class={cn('size-4', className)} />
{/snippet}
<CalendarPrimitive.NextButton
bind:ref
class={cn(
buttonVariants({ variant }),
'size-(--cell-size) rounded-full border border-zinc-800 bg-transparent p-0 text-zinc-300 select-none hover:border-[#d0e891] hover:bg-[#b9d765] hover:text-[#101207] disabled:opacity-50 rtl:rotate-180',
className
)}
{...restProps}
>
{#if children}
{@render children?.()}
{:else}
{@render Fallback()}
{/if}
</CalendarPrimitive.NextButton>
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import CaretLeftIcon from 'phosphor-svelte/lib/CaretLeft';
import { buttonVariants, type ButtonVariant } from '$UI$/button/index.js';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
children,
variant = 'ghost',
...restProps
}: CalendarPrimitive.PrevButtonProps & {
variant?: ButtonVariant;
} = $props();
</script>
{#snippet Fallback()}
<CaretLeftIcon class={cn('size-4', className)} />
{/snippet}
<CalendarPrimitive.PrevButton
bind:ref
class={cn(
buttonVariants({ variant }),
'size-(--cell-size) rounded-full border border-zinc-800 bg-transparent p-0 text-zinc-300 select-none hover:border-[#d0e891] hover:bg-[#b9d765] hover:text-[#101207] disabled:opacity-50 rtl:rotate-180',
className
)}
{...restProps}
>
{#if children}
{@render children?.()}
{:else}
{@render Fallback()}
{/if}
</CalendarPrimitive.PrevButton>
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$UTILS$.js';
import CaretDownIcon from 'phosphor-svelte/lib/CaretDown';
let {
ref = $bindable(null),
class: className,
value,
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.YearSelectProps> = $props();
</script>
<span
class={cn(
'relative flex border border-zinc-800 bg-zinc-950 text-zinc-100 has-focus:border-zinc-300 has-focus:ring-2 has-focus:ring-zinc-300/30',
className
)}
>
<CalendarPrimitive.YearSelect
bind:ref
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
{...restProps}
>
{#snippet child({ props, yearItems, selectedYearItem })}
<select {...props} {value}>
{#each yearItems as yearItem (yearItem.value)}
<option
value={yearItem.value}
selected={value !== undefined
? yearItem.value === value
: yearItem.value === selectedYearItem.value}
>
{yearItem.label}
</option>
{/each}
</select>
<span
class="flex h-(--cell-size) items-center gap-1 ps-2 pe-1 text-sm font-medium text-zinc-100 select-none [&>svg]:size-3.5 [&>svg]:text-zinc-500"
aria-hidden="true"
>
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
<CaretDownIcon class={cn('size-4', className)} />
</span>
{/snippet}
</CalendarPrimitive.YearSelect>
</span>
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import * as Calendar from './index.js';
import { cn, type WithoutChildrenOrChild } from '$UTILS$.js';
import type { ButtonVariant } from '../button/button.svelte';
import { isEqualMonth, type DateValue } from '@internationalized/date';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
value = $bindable(),
placeholder = $bindable(),
class: className,
weekdayFormat = 'short',
buttonVariant = 'ghost',
captionLayout = 'label',
locale = 'en-US',
months: monthsProp,
years,
monthFormat: monthFormatProp,
yearFormat = 'numeric',
day,
disableDaysOutsideMonth = false,
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
buttonVariant?: ButtonVariant;
captionLayout?: 'dropdown' | 'dropdown-months' | 'dropdown-years' | 'label';
months?: CalendarPrimitive.MonthSelectProps['months'];
years?: CalendarPrimitive.YearSelectProps['years'];
monthFormat?: CalendarPrimitive.MonthSelectProps['monthFormat'];
yearFormat?: CalendarPrimitive.YearSelectProps['yearFormat'];
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
} = $props();
const monthFormat = $derived.by(() => {
if (monthFormatProp) return monthFormatProp;
if (captionLayout.startsWith('dropdown')) return 'short';
return 'long';
});
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<CalendarPrimitive.Root
bind:value={value as never}
bind:ref
bind:placeholder
{weekdayFormat}
{disableDaysOutsideMonth}
class={cn(
'group/calendar border border-zinc-800 bg-background p-3 text-zinc-100 [--cell-radius:0] [--cell-size:--spacing(8)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent',
className
)}
{locale}
{monthFormat}
{yearFormat}
{...restProps}
>
{#snippet children({ months, weekdays })}
<Calendar.Months>
<Calendar.Nav>
<Calendar.PrevButton variant={buttonVariant} />
<Calendar.NextButton variant={buttonVariant} />
</Calendar.Nav>
{#each months as month, monthIndex (month)}
<Calendar.Month>
<Calendar.Header>
<Calendar.Caption
{captionLayout}
months={monthsProp}
{monthFormat}
{years}
{yearFormat}
month={month.value}
bind:placeholder
{locale}
{monthIndex}
/>
</Calendar.Header>
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow class="select-none border-b border-zinc-900">
{#each weekdays as weekday, i (i)}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates (weekDates)}
<Calendar.GridRow class="w-full">
{#each weekDates as date (date)}
<Calendar.Cell {date} month={month.value}>
{#if day}
{@render day({
day: date,
outsideMonth: !isEqualMonth(date, month.value)
})}
{:else}
<Calendar.Day />
{/if}
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
</Calendar.Month>
{/each}
</Calendar.Months>
{/snippet}
</CalendarPrimitive.Root>
import Root from './calendar.svelte';
import Cell from './calendar-cell.svelte';
import Day from './calendar-day.svelte';
import Grid from './calendar-grid.svelte';
import Header from './calendar-header.svelte';
import Months from './calendar-months.svelte';
import GridRow from './calendar-grid-row.svelte';
import Heading from './calendar-heading.svelte';
import GridBody from './calendar-grid-body.svelte';
import GridHead from './calendar-grid-head.svelte';
import HeadCell from './calendar-head-cell.svelte';
import NextButton from './calendar-next-button.svelte';
import PrevButton from './calendar-prev-button.svelte';
import MonthSelect from './calendar-month-select.svelte';
import YearSelect from './calendar-year-select.svelte';
import Month from './calendar-month.svelte';
import Nav from './calendar-nav.svelte';
import Caption from './calendar-caption.svelte';
export {
Day,
Cell,
Grid,
Header,
Months,
GridRow,
Heading,
GridBody,
GridHead,
HeadCell,
NextButton,
PrevButton,
Nav,
Month,
YearSelect,
MonthSelect,
Caption,
//
Root as Calendar
};
About
The <Calendar /> component is built on top of the Bits UI 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.
Examples
Range Calendar
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
<script lang="ts">
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import { CalendarDate } from "@internationalized/date";
let value = $state<CalendarDate | undefined>(new CalendarDate(2025, 6, 12));
</script>
<Calendar
type="single"
bind:value
class="rounded-lg border shadow-sm"
numberOfMonths={2}
/> Month and Year Selector
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
<script lang="ts">
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import * as Select from "$lib/components/ui/select/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import { CalendarDate } from "@internationalized/date";
import type { ComponentProps } from "svelte";
let value = $state<CalendarDate>(new CalendarDate(2025, 6, 12));
let dropdown =
$state<ComponentProps<typeof Calendar>["captionLayout"]>("dropdown");
const dropdownOptions = [
{
label: "Month and Year",
value: "dropdown"
},
{
label: "Month Only",
value: "dropdown-months"
},
{
label: "Year Only",
value: "dropdown-years"
}
];
const selectedDropdown = $derived(
dropdownOptions.find((option) => option.value === dropdown)?.label ??
"Dropdown"
);
const id = $props.id();
</script>
<div class="flex flex-col gap-4">
<Calendar
type="single"
bind:value
class="rounded-lg border shadow-sm"
captionLayout={dropdown}
/>
<div class="flex flex-col gap-3">
<Label for="{id}-dropdown" class="px-1">Dropdown</Label>
<Select.Root type="single" bind:value={dropdown}>
<Select.Trigger id="{id}-dropdown" size="sm" class="bg-background w-full">
{selectedDropdown}
</Select.Trigger>
<Select.Content align="center">
{#each dropdownOptions as option (option.value)}
<Select.Item value={option.value}>{option.label}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div> Date of Birth Picker
<script lang="ts">
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import * as Popover from "$lib/components/ui/popover/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import {
getLocalTimeZone,
today,
type CalendarDate
} from "@internationalized/date";
const id = $props.id();
let open = $state(false);
let value = $state<CalendarDate | undefined>();
</script>
<div class="flex flex-col gap-3">
<Label for="{id}-date" class="px-1">Date of birth</Label>
<Popover.Root bind:open>
<Popover.Trigger id="{id}-date">
{#snippet child({ props })}
<Button
{...props}
variant="outline"
class="w-48 justify-between font-normal"
>
{value
? value.toDate(getLocalTimeZone()).toLocaleDateString()
: "Select date"}
<ChevronDownIcon />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto overflow-hidden p-0" align="start">
<Calendar
type="single"
bind:value
captionLayout="dropdown"
onValueChange={() => {
open = false;
}}
maxValue={today(getLocalTimeZone())}
/>
</Popover.Content>
</Popover.Root>
</div> Date and Time Picker
<script lang="ts">
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import * as Popover from "$lib/components/ui/popover/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import { getLocalTimeZone } from "@internationalized/date";
import type { CalendarDate } from "@internationalized/date";
const id = $props.id();
let open = $state(false);
let value = $state<CalendarDate | undefined>();
</script>
<div class="flex gap-4">
<div class="flex flex-col gap-3">
<Label for="{id}-date" class="px-1">Date</Label>
<Popover.Root bind:open>
<Popover.Trigger id="{id}-date">
{#snippet child({ props })}
<Button
{...props}
variant="outline"
class="w-32 justify-between font-normal"
>
{value
? value.toDate(getLocalTimeZone()).toLocaleDateString()
: "Select date"}
<ChevronDownIcon />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto overflow-hidden p-0" align="start">
<Calendar
type="single"
bind:value
onValueChange={() => {
open = false;
}}
captionLayout="dropdown"
/>
</Popover.Content>
</Popover.Root>
</div>
<div class="flex flex-col gap-3">
<Label for="{id}-time" class="px-1">Time</Label>
<Input
type="time"
id="{id}-time"
step="1"
value="10:30:00"
class="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</div>
</div> Natural Language Picker
This component uses the chrono-node library to parse natural language dates.
<script lang="ts">
import { Label } from "$lib/components/ui/label/index.js";
import * as Popover from "$lib/components/ui/popover/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { Calendar } from "$lib/components/ui/calendar/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import CalendarIcon from "@lucide/svelte/icons/calendar";
import { parseDate } from "chrono-node";
import {
CalendarDate,
getLocalTimeZone,
type DateValue
} from "@internationalized/date";
import { untrack } from "svelte";
function formatDate(date: DateValue | undefined) {
if (!date) return "";
return date.toDate(getLocalTimeZone()).toLocaleDateString("en-US", {
day: "2-digit",
month: "long",
year: "numeric"
});
}
const id = $props.id();
let open = $state(false);
let inputValue = $state("In 2 days");
let value = $state<DateValue | undefined>(
untrack(() => {
const date = parseDate(inputValue);
if (date)
return new CalendarDate(
date.getFullYear(),
date.getMonth() + 1,
date.getDate()
);
return undefined;
})
);
</script>
<div class="flex flex-col gap-3">
<Label for="{id}-date" class="px-1">Schedule Date</Label>
<div class="relative flex gap-2">
<Input
id="date"
bind:value={
() => inputValue,
(v) => {
inputValue = v;
const date = parseDate(v);
if (date) {
value = new CalendarDate(
date.getFullYear(),
date.getMonth() + 1,
date.getDate()
);
}
}
}
placeholder="Tomorrow or next week"
class="bg-background pe-10"
onkeydown={(e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
open = true;
}
}}
/>
<Popover.Root bind:open>
<Popover.Trigger id="{id}-date-picker">
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
class="absolute end-2 top-1/2 size-6 -translate-y-1/2"
>
<CalendarIcon class="size-3.5" />
<span class="sr-only">Select date</span>
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto overflow-hidden p-0" align="end">
<Calendar
type="single"
bind:value
captionLayout="dropdown"
onValueChange={(v) => {
inputValue = formatDate(v);
open = false;
}}
/>
</Popover.Content>
</Popover.Root>
</div>
<div class="text-muted-foreground px-1 text-sm">
Your post will be published on
<span class="font-medium">{formatDate(value)}</span>.
</div>
</div> Upgrade Guide
You can upgrade to the latest version of the <Calendar /> component by running the following command:
pnpm dlx shadcn-svelte@latest add calendar When you're prompted to overwrite the existing files, select Yes. If you have made any changes to the Calendar component, you will need to merge your changes with the new version.
Installing Blocks
After upgrading the Calendar component, you can add the new blocks with the following:
pnpm dlx shadcn-svelte@latest add calendar-02 This will add the latest version of the calendar blocks.