Building Blocks for the Web
Clean, modern building blocks. Works with all Svelte projects. Copy and paste into your apps. Open Source. Free forever.
Files
<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" />
A simple calendar.
calendar-01
Files
<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} />
Multiple months with single selection.
calendar-02
Files
<script lang="ts">
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import { CalendarDate, type DateValue } from "@internationalized/date";
let value = $state<CalendarDate[]>([
new CalendarDate(2025, 6, 12),
new CalendarDate(2025, 7, 24),
]);
// svelte-ignore state_referenced_locally
let placeholder = $state<DateValue>(value[0]);
</script>
<Calendar
type="multiple"
bind:value
bind:placeholder
maxDays={5}
class="rounded-lg border shadow-sm"
numberOfMonths={2}
/>
Multiple months with multiple selection.
calendar-03
Files
<script lang="ts">
import RangeCalendar from "$lib/components/ui/range-calendar/range-calendar.svelte";
import { CalendarDate } from "@internationalized/date";
import type { DateRange } from "bits-ui";
let value = $state<DateRange>({
start: new CalendarDate(2025, 6, 9),
end: new CalendarDate(2025, 6, 26),
});
</script>
<RangeCalendar bind:value class="rounded-lg border shadow-sm" />
Single month with range selection
calendar-04
Files
<script lang="ts">
import RangeCalendar from "$lib/components/ui/range-calendar/range-calendar.svelte";
import { CalendarDate } from "@internationalized/date";
import type { DateRange } from "bits-ui";
let value = $state<DateRange>({
start: new CalendarDate(2025, 6, 12),
end: new CalendarDate(2025, 7, 15),
});
</script>
<RangeCalendar bind:value class="rounded-lg border shadow-sm" numberOfMonths={2} />
Multiple months with range selection
calendar-05
Files
<script lang="ts">
import RangeCalendar from "$lib/components/ui/range-calendar/range-calendar.svelte";
import { CalendarDate } from "@internationalized/date";
import type { DateRange } from "bits-ui";
let value = $state<DateRange>({
start: new CalendarDate(2025, 6, 12),
end: new CalendarDate(2025, 6, 26),
});
</script>
<div class="flex min-w-0 flex-col gap-2">
<RangeCalendar bind:value minDays={5} class="rounded-lg border shadow-sm" />
<div class="text-muted-foreground text-center text-xs">A minimum of 5 days is required</div>
</div>
Range selection with minimum days
calendar-06
Files
<script lang="ts">
import RangeCalendar from "$lib/components/ui/range-calendar/range-calendar.svelte";
import { CalendarDate } from "@internationalized/date";
import type { DateRange } from "bits-ui";
let value = $state<DateRange>({
start: new CalendarDate(2025, 6, 18),
end: new CalendarDate(2025, 7, 7),
});
</script>
<div class="flex min-w-0 flex-col gap-2">
<RangeCalendar
bind:value
minDays={2}
maxDays={20}
numberOfMonths={2}
class="rounded-lg border shadow-sm"
/>
<div class="text-muted-foreground text-center text-xs">
Your stay must be between 2 and 20 nights
</div>
</div>
Range selection with minimum and maximum days
calendar-07
Files
<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"
minValue={new CalendarDate(2025, 6, 12)}
/>
Calendar with disabled days
calendar-08
Files
<script lang="ts">
import RangeCalendar from "$lib/components/ui/range-calendar/range-calendar.svelte";
import { CalendarDate, isWeekend } from "@internationalized/date";
import type { DateRange } from "bits-ui";
let value = $state<DateRange>({
start: new CalendarDate(2025, 6, 17),
end: new CalendarDate(2025, 6, 20),
});
</script>
<div class="flex min-w-0 flex-col gap-2">
<RangeCalendar
bind:value
numberOfMonths={2}
isDateDisabled={(date) => isWeekend(date, "en-US")}
class="rounded-lg border shadow-sm"
excludeDisabled
/>
<div class="text-muted-foreground text-center text-xs">
Your stay cannot extend through the weekend
</div>
</div>
Calendar with disabled weekends
calendar-09
Files
<script lang="ts">
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import * as Card from "$lib/components/ui/card/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { CalendarDate, getLocalTimeZone, today } from "@internationalized/date";
let value = $state<CalendarDate | undefined>(new CalendarDate(2025, 6, 12));
</script>
<Card.Root>
<Card.Header>
<Card.Title>Appointment</Card.Title>
<Card.Description>Find a date.</Card.Description>
<Card.Action>
<Button size="sm" variant="outline" onclick={() => (value = today(getLocalTimeZone()))}>
Today
</Button>
</Card.Action>
</Card.Header>
<Card.Content>
<Calendar type="single" bind:value class="bg-transparent p-0" />
</Card.Content>
</Card.Root>
Today button
calendar-10
Files
<script lang="ts">
import RangeCalendar from "$lib/components/ui/range-calendar/range-calendar.svelte";
import { CalendarDate } from "@internationalized/date";
import type { DateRange } from "bits-ui";
let value = $state<DateRange>({
start: new CalendarDate(2025, 6, 17),
end: new CalendarDate(2025, 6, 20),
});
</script>
<div class="flex min-w-0 flex-col gap-2">
<RangeCalendar
bind:value
numberOfMonths={2}
minValue={new CalendarDate(2025, 6, 1)}
maxValue={new CalendarDate(2025, 7, 31)}
class="rounded-lg border shadow-sm"
/>
<div class="text-muted-foreground text-center text-xs">We are open in June and July only.</div>
</div>
Start and end of month
calendar-11
Files
<script lang="ts">
import * as Card from "$lib/components/ui/card/index.js";
import * as Select from "$lib/components/ui/select/index.js";
import { CalendarDate } from "@internationalized/date";
import RangeCalendar from "../ui/range-calendar/range-calendar.svelte";
import type { DateRange } from "bits-ui";
let value = $state<DateRange | undefined>({
start: new CalendarDate(2025, 9, 9),
end: new CalendarDate(2025, 9, 17),
});
const localizedStrings = {
en: {
title: "Book an appointment",
description: "Select the dates for your appointment",
},
es: {
title: "Reserva una cita",
description: "Selecciona las fechas para tu cita",
},
} as const;
let locale = $state<keyof typeof localizedStrings>("es");
const languageOptions = [
{
label: "English",
value: "en",
},
{
label: "Español",
value: "es",
},
];
const selectedLanguage = $derived(
languageOptions.find((option) => option.value === locale)?.label ?? "Language"
);
</script>
<Card.Root>
<Card.Header>
<Card.Title>{localizedStrings[locale].title}</Card.Title>
<Card.Description>{localizedStrings[locale].description}</Card.Description>
<Card.Action>
<Select.Root type="single" bind:value={locale}>
<Select.Trigger class="w-[100px]" aria-label="Select language">
{selectedLanguage}
</Select.Trigger>
<Select.Content align="end">
{#each languageOptions as option (option.value)}
<Select.Item value={option.value}>{option.label}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</Card.Action>
</Card.Header>
<Card.Content>
<RangeCalendar
bind:value
numberOfMonths={2}
{locale}
class="bg-transparent p-0"
buttonVariant="outline"
/>
</Card.Content>
</Card.Root>
Localized calendar
calendar-12
Files
<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>
With Month and Year Dropdown
calendar-13
Files
<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));
const bookedDates = Array.from({ length: 12 }, (_, i) => new CalendarDate(2025, 6, 15 + i));
</script>
<Calendar
type="single"
bind:value
class="rounded-lg border shadow-sm"
isDateUnavailable={(date) => bookedDates.some((d) => d.compare(date) === 0)}
/>
With Booked/Unavailable Days
calendar-14
Files
<script lang="ts">
import Construction from "@lucide/svelte/icons/construction";
</script>
<div class="flex h-full flex-col items-center justify-center gap-4">
<Construction class="size-10" />
<span>This block is under construction. Check back soon!</span>
</div>
With Week Numbers
calendar-15
Files
<script lang="ts">
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import { CalendarDate } from "@internationalized/date";
import * as Card from "$lib/components/ui/card/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import Clock2Icon from "@lucide/svelte/icons/clock-2";
let value = $state<CalendarDate | undefined>(new CalendarDate(2025, 6, 12));
</script>
<Card.Root class="w-fit py-4">
<Card.Content class="px-4">
<Calendar type="single" bind:value class="bg-transparent p-0" />
</Card.Content>
<Card.Footer class="flex flex-col gap-6 border-t px-4 !pt-4">
<div class="flex w-full flex-col gap-3">
<Label for="time-from">Start Time</Label>
<div class="relative flex w-full items-center gap-2">
<Clock2Icon
class="text-muted-foreground pointer-events-none absolute left-2.5 size-4 select-none"
/>
<Input
id="time-from"
type="time"
step="1"
value="10:30:00"
class="appearance-none pl-8 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</div>
</div>
<div class="flex w-full flex-col gap-3">
<Label for="time-to">End Time</Label>
<div class="relative flex w-full items-center gap-2">
<Clock2Icon
class="text-muted-foreground pointer-events-none absolute left-2.5 size-4 select-none"
/>
<Input
id="time-to"
type="time"
step="1"
value="12:30:00"
class="appearance-none pl-8 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</div>
</div>
</Card.Footer>
</Card.Root>
With time picker
calendar-16
Files
<script lang="ts">
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import { CalendarDate } from "@internationalized/date";
import * as Card from "$lib/components/ui/card/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import { Input } from "$lib/components/ui/input/index.js";
let value = $state<CalendarDate | undefined>(new CalendarDate(2025, 6, 12));
</script>
<Card.Root class="w-fit py-4">
<Card.Content class="px-4">
<Calendar
type="single"
bind:value
class="bg-transparent p-0 [--cell-size:--spacing(10.5)]"
/>
</Card.Content>
<Card.Footer class="*:[div]:w-full flex gap-2 border-t px-4 !pt-4">
<div>
<Label for="time-from" class="sr-only">Start Time</Label>
<Input
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"
/>
</div>
<span>-</span>
<div>
<Label for="time-to" class="sr-only">End Time</Label>
<Input
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"
/>
</div>
</Card.Footer>
</Card.Root>
With time picker inline
calendar-17
Files
<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 [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/>
Variable size
calendar-18
Files
<script lang="ts">
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import { CalendarDate, getLocalTimeZone, today } from "@internationalized/date";
import { Button } from "$lib/components/ui/button/index.js";
import * as Card from "$lib/components/ui/card/index.js";
let todayDate = today(getLocalTimeZone());
let value = $state<CalendarDate | undefined>(new CalendarDate(2025, 6, 12));
</script>
<Card.Root class="max-w-[300px] py-4">
<Card.Content class="px-4">
<Calendar
type="single"
bind:value
class="bg-transparent p-0 [--cell-size:--spacing(9.5)]"
/>
</Card.Content>
<Card.Footer class="flex flex-wrap gap-2 border-t px-4 !pt-4">
{#each [{ 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 }] as preset (preset.value)}
<Button
variant="outline"
size="sm"
class="flex-1"
onclick={() => {
value = todayDate?.add({ days: preset.value });
}}
>
{preset.label}
</Button>
{/each}
</Card.Footer>
</Card.Root>
With presets
calendar-19
Files
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import * as Card from "$lib/components/ui/card/index.js";
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import { CalendarDate, getLocalTimeZone } from "@internationalized/date";
let value = $state<CalendarDate | undefined>(new CalendarDate(2025, 6, 12));
let selectedTime = $state<string | null>("10:00");
const bookedDates = Array.from({ length: 3 }, (_, i) => new CalendarDate(2025, 6, 17 + i));
const timeSlots = Array.from({ length: 37 }, (_, i) => {
const totalMinutes = i * 15;
const hour = Math.floor(totalMinutes / 60) + 9;
const minute = totalMinutes % 60;
return `${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}`;
});
</script>
<Card.Root class="gap-0 p-0">
<Card.Content class="relative p-0 md:pr-48">
<div class="p-6">
<Calendar
type="single"
bind:value
isDateUnavailable={(date) => bookedDates.some((d) => d.compare(date) === 0)}
class="data-unavailable:line-through data-unavailable:opacity-100 bg-transparent p-0 [--cell-size:--spacing(10)] md:[--cell-size:--spacing(12)] [&_[data-outside-month]]:hidden"
weekdayFormat="short"
/>
</div>
<div
class="no-scrollbar inset-y-0 right-0 flex max-h-72 w-full scroll-pb-6 flex-col gap-4 overflow-y-auto border-t p-6 md:absolute md:max-h-none md:w-48 md:border-l md:border-t-0"
>
<div class="grid gap-2">
{#each timeSlots as time (time)}
<Button
variant={selectedTime === time ? "default" : "outline"}
onclick={() => (selectedTime = time)}
class="w-full shadow-none"
>
{time}
</Button>
{/each}
</div>
</div>
</Card.Content>
<Card.Footer class="flex flex-col gap-4 border-t !py-5 px-6 md:flex-row">
<div class="text-sm">
{#if value && selectedTime}
Your meeting is booked for
<span class="font-medium">
{value.toDate(getLocalTimeZone()).toLocaleDateString("en-US", {
weekday: "long",
day: "numeric",
month: "short",
})}
</span>
at <span class="font-medium">{selectedTime}</span>.
{:else}
Select a date and time for your meeting.
{/if}
</div>
<Button
disabled={!value || !selectedTime}
class="w-full md:ml-auto md:w-auto"
variant="outline"
>
Continue
</Button>
</Card.Footer>
</Card.Root>
With time presets
calendar-20
Files
<script lang="ts">
import RangeCalendar from "$lib/components/ui/range-calendar/range-calendar.svelte";
import RangeCalendarDay from "$lib/components/ui/range-calendar/range-calendar-day.svelte";
import { CalendarDate, isWeekend } from "@internationalized/date";
import type { DateRange } from "bits-ui";
let value = $state<DateRange | undefined>({
start: new CalendarDate(2025, 6, 12),
end: new CalendarDate(2025, 6, 17),
});
</script>
<RangeCalendar
bind:value
class="rounded-lg border shadow-sm [--cell-size:--spacing(11)] md:[--cell-size:--spacing(13)]"
monthFormat="long"
captionLayout="dropdown"
>
{#snippet day({ day, outsideMonth })}
{@const dayIsWeekend = isWeekend(day, "en-US")}
<RangeCalendarDay class="flex flex-col items-center">
{day.day}
{#if !outsideMonth}
<span>
{dayIsWeekend ? "$220" : "$100"}
</span>
{/if}
</RangeCalendarDay>
{/snippet}
</RangeCalendar>
Custom days and formatters
calendar-21
Files
<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 picker
calendar-22
Files
<script lang="ts">
import RangeCalendar from "$lib/components/ui/range-calendar/range-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 } from "@internationalized/date";
import type { DateRange } from "bits-ui";
const id = $props.id();
let open = $state(false);
let value = $state<DateRange | undefined>();
</script>
<div class="flex flex-col gap-3">
<Label for="{id}-dates" class="px-1">Select your stay</Label>
<Popover.Root bind:open>
<Popover.Trigger id="{id}-dates">
{#snippet child({ props })}
<Button {...props} variant="outline" class="w-56 justify-between font-normal">
{value?.start && value?.end
? `${value.start.toDate(getLocalTimeZone()).toLocaleDateString()} - ${value.end.toDate(getLocalTimeZone()).toLocaleDateString()}`
: "Select date"}
<ChevronDownIcon />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto overflow-hidden p-0" align="start">
<RangeCalendar bind:value captionLayout="dropdown" />
</Popover.Content>
</Popover.Root>
</div>
Date range picker
calendar-23
Files
<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>
Date and Time picker
calendar-24
Files
<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 flex-col gap-6">
<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-full 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;
}}
/>
</Popover.Content>
</Popover.Root>
</div>
<div class="flex gap-4">
<div class="flex flex-col gap-3">
<Label for="{id}-time-from" class="px-1">From</Label>
<Input
type="time"
id="{id}-time-from"
step="1"
value="10:30:00"
class="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</div>
<div class="flex flex-col gap-3">
<Label for="{id}-time-to" class="px-1">To</Label>
<Input
type="time"
id="{id}-time-to"
step="1"
value="12:30:00"
class="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</div>
</div>
</div>
Date and Time range picker
calendar-25
Files
<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 openFrom = $state(false);
let openTo = $state(false);
let valueFrom = $state<CalendarDate | undefined>();
let valueTo = $state<CalendarDate | undefined>();
</script>
<div class="flex flex-col gap-6">
<div class="flex gap-4">
<div class="flex flex-1 flex-col gap-3">
<Label for="{id}-date-from" class="px-1">Check-in</Label>
<Popover.Root bind:open={openFrom}>
<Popover.Trigger id="{id}-date-from">
{#snippet child({ props })}
<Button
{...props}
variant="outline"
class="w-full justify-between font-normal"
>
{valueFrom
? valueFrom.toDate(getLocalTimeZone()).toLocaleDateString("en-US", {
day: "2-digit",
month: "short",
year: "numeric",
})
: "Select date"}
<ChevronDownIcon />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto overflow-hidden p-0" align="start">
<Calendar
type="single"
bind:value={valueFrom}
captionLayout="dropdown"
onValueChange={() => {
openFrom = false;
}}
/>
</Popover.Content>
</Popover.Root>
</div>
<div class="flex flex-col gap-3">
<Label for="{id}-time-from" class="invisible px-1">From</Label>
<Input
type="time"
id="{id}-time-from"
step="1"
value="10:30:00"
class="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</div>
</div>
<div class="flex gap-4">
<div class="flex flex-1 flex-col gap-3">
<Label for="{id}-date-to" class="px-1">Check-out</Label>
<Popover.Root bind:open={openTo}>
<Popover.Trigger id="{id}-date-to">
{#snippet child({ props })}
<Button
{...props}
variant="outline"
class="w-full justify-between font-normal"
>
{valueTo
? valueTo.toDate(getLocalTimeZone()).toLocaleDateString("en-US", {
day: "2-digit",
month: "short",
year: "numeric",
})
: "Select date"}
<ChevronDownIcon />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto overflow-hidden p-0" align="start">
<Calendar
type="single"
bind:value={valueTo}
captionLayout="dropdown"
onValueChange={() => {
openTo = false;
}}
isDateDisabled={(date) => {
return (valueFrom && date.compare(valueFrom) < 0) ?? false;
}}
/>
</Popover.Content>
</Popover.Root>
</div>
<div class="flex flex-col gap-3">
<Label for="{id}-time-to" class="invisible px-1">To</Label>
<Input
type="time"
id="{id}-time-to"
step="1"
value="12:30:00"
class="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</div>
</div>
</div>
Date range picker with time
calendar-26
Files
<script lang="ts">
import CalendarIcon from "@lucide/svelte/icons/calendar";
import * as Chart from "$lib/components/ui/chart/index.js";
import * as Card from "$lib/components/ui/card/index.js";
import * as Popover from "$lib/components/ui/popover/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { BarChart, Highlight, type ChartContextValue } from "layerchart";
import RangeCalendar from "$lib/components/ui/range-calendar/range-calendar.svelte";
import type { DateRange } from "bits-ui";
import { CalendarDate, getLocalTimeZone } from "@internationalized/date";
import { scaleBand } from "d3-scale";
import { cubicInOut } from "svelte/easing";
let value = $state<DateRange | undefined>({
start: new CalendarDate(2025, 6, 5),
end: new CalendarDate(2025, 6, 20),
});
const chartData = [
{ date: new Date("2025-06-01"), visitors: 178 },
{ date: new Date("2025-06-02"), visitors: 470 },
{ date: new Date("2025-06-03"), visitors: 103 },
{ date: new Date("2025-06-04"), visitors: 439 },
{ date: new Date("2025-06-05"), visitors: 88 },
{ date: new Date("2025-06-06"), visitors: 294 },
{ date: new Date("2025-06-07"), visitors: 323 },
{ date: new Date("2025-06-08"), visitors: 385 },
{ date: new Date("2025-06-09"), visitors: 438 },
{ date: new Date("2025-06-10"), visitors: 155 },
{ date: new Date("2025-06-11"), visitors: 92 },
{ date: new Date("2025-06-12"), visitors: 492 },
{ date: new Date("2025-06-13"), visitors: 81 },
{ date: new Date("2025-06-14"), visitors: 426 },
{ date: new Date("2025-06-15"), visitors: 307 },
{ date: new Date("2025-06-16"), visitors: 371 },
{ date: new Date("2025-06-17"), visitors: 475 },
{ date: new Date("2025-06-18"), visitors: 107 },
{ date: new Date("2025-06-19"), visitors: 341 },
{ date: new Date("2025-06-20"), visitors: 408 },
{ date: new Date("2025-06-21"), visitors: 169 },
{ date: new Date("2025-06-22"), visitors: 317 },
{ date: new Date("2025-06-23"), visitors: 480 },
{ date: new Date("2025-06-24"), visitors: 132 },
{ date: new Date("2025-06-25"), visitors: 141 },
{ date: new Date("2025-06-26"), visitors: 434 },
{ date: new Date("2025-06-27"), visitors: 448 },
{ date: new Date("2025-06-28"), visitors: 149 },
{ date: new Date("2025-06-29"), visitors: 103 },
{ date: new Date("2025-06-30"), visitors: 446 },
];
const total = chartData.reduce((acc, curr) => acc + curr.visitors, 0);
const chartConfig = {
visitors: {
label: "Visitors",
color: "var(--color-primary)",
},
} satisfies Chart.ChartConfig;
const filteredData = $derived.by(() => {
if (!value?.start || !value?.end) return chartData;
return chartData.filter(({ date }) => {
const dateObj = new Date(date);
if (!value) return true;
const startDate = value.start!.toDate(getLocalTimeZone());
const endDate = value.end!.toDate(getLocalTimeZone());
// set end date to end of day to include the full day
endDate.setHours(23, 59, 59, 999);
return dateObj >= startDate && dateObj <= endDate;
});
});
let context = $state<ChartContextValue>();
</script>
<Card.Root class="@container/card w-full max-w-xl">
<Card.Header class="@md/card:grid flex flex-col border-b">
<Card.Title>Web Analytics</Card.Title>
<Card.Description>Showing total visitors for this month.</Card.Description>
<Card.Action class="@md/card:mt-0 mt-2">
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline">
<CalendarIcon />
{value?.start && value?.end
? `${value.start.toDate(getLocalTimeZone()).toLocaleDateString()} - ${value.end.toDate(getLocalTimeZone()).toLocaleDateString()}`
: "June 2025"}
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto overflow-hidden p-0" align="end">
<RangeCalendar
class="w-full"
fixedWeeks
bind:value
minValue={new CalendarDate(2025, 6, 1)}
maxValue={new CalendarDate(2025, 6, 31)}
/>
</Popover.Content>
</Popover.Root>
</Card.Action>
</Card.Header>
<Card.Content class="px-4">
<Chart.Container config={chartConfig} class="aspect-auto h-[250px] w-full">
<BarChart
bind:context
data={filteredData}
xScale={scaleBand().padding(0.25)}
x="date"
axis="x"
y="visitors"
props={{
bars: {
stroke: "none",
rounded: "all",
radius: 4,
// use the height of the chart to animate the bars
initialY: context?.height,
initialHeight: 0,
motion: {
x: { type: "tween", duration: 500, easing: cubicInOut },
width: { type: "tween", duration: 500, easing: cubicInOut },
height: { type: "tween", duration: 500, easing: cubicInOut },
y: { type: "tween", duration: 500, easing: cubicInOut },
},
},
xAxis: { format: (d) => d.toLocaleDateString("en-US", { day: "numeric" }) },
}}
>
{#snippet belowMarks()}
<Highlight area={{ class: "fill-muted" }} />
{/snippet}
{#snippet tooltip()}
<Chart.Tooltip
class="w-[150px]"
nameKey="visitors"
labelFormatter={(d) =>
d.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})}
/>
{/snippet}
</BarChart>
</Chart.Container>
</Card.Content>
<Card.Footer class="border-t">
<div class="text-sm">
You had
<span class="font-semibold">{total.toLocaleString()}</span>
visitors for the month of June.
</div>
</Card.Footer>
</Card.Root>
Chart filter
calendar-27
Files
<script lang="ts">
import CalendarIcon from "@lucide/svelte/icons/calendar";
import { CalendarDate, getLocalTimeZone, type DateValue } from "@internationalized/date";
import { untrack } from "svelte";
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import { Input } from "$lib/components/ui/input/index.js";
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";
function formatDate(date: DateValue | undefined) {
if (!date) return "";
return date.toDate(getLocalTimeZone()).toLocaleDateString("en-US", {
day: "2-digit",
month: "long",
year: "numeric",
});
}
function isValidDate(date: Date | undefined): date is Date {
if (!date) return false;
return !isNaN(date.getTime());
}
const id = $props.id();
let value = $state<DateValue | undefined>(new CalendarDate(2025, 6, 1));
let open = $state(false);
let inputValue = $state(untrack(() => formatDate(value)));
</script>
<div class="flex flex-col gap-3">
<Label for="{id}-date" class="px-1">Subscription Date</Label>
<div class="relative flex gap-2">
<Input
id="{id}-date"
placeholder="June 01, 2025"
class="bg-background pr-10"
bind:value={
() => inputValue,
(v) => {
const date = new Date(v);
inputValue = v;
if (isValidDate(date)) {
value = new CalendarDate(
date.getFullYear(),
date.getMonth(),
date.getDate()
);
}
}
}
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 right-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"
alignOffset={-8}
sideOffset={10}
>
<Calendar
type="single"
bind:value
captionLayout="dropdown"
onValueChange={(v) => {
inputValue = formatDate(v);
open = false;
}}
/>
</Popover.Content>
</Popover.Root>
</div>
</div>
Input with date picker
calendar-28
Files
<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 pr-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 right-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>
Natural language date picker
calendar-29
Files
<script lang="ts">
import * as Popover from "$lib/components/ui/popover/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import RangeCalendar from "$lib/components/ui/range-calendar/range-calendar.svelte";
import { Label } from "$lib/components/ui/label/index.js";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import type { DateRange } from "bits-ui";
import { CalendarDate, getLocalTimeZone, type DateValue } from "@internationalized/date";
import { formatDateRange } from "little-date";
const id = $props.id();
let value = $state<DateRange | undefined>({
start: new CalendarDate(2025, 6, 4),
end: new CalendarDate(2025, 6, 10),
});
function formatRange(start: DateValue, end: DateValue) {
return formatDateRange(start.toDate(getLocalTimeZone()), end.toDate(getLocalTimeZone()), {
includeTime: false,
});
}
</script>
<div class="flex flex-col gap-3">
<Label for="{id}-dates" class="px-1">Select your stay</Label>
<Popover.Root>
<Popover.Trigger id="{id}-dates">
{#snippet child({ props })}
<Button {...props} variant="outline" class="w-56 justify-between font-normal">
{#if value?.start && value?.end}
{formatRange(value.start, value.end)}
{:else}
Select date
{/if}
<ChevronDownIcon />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto overflow-hidden p-0" align="start">
<RangeCalendar bind:value captionLayout="dropdown" />
</Popover.Content>
</Popover.Root>
</div>
With little-date
calendar-30
Files
<script lang="ts">
import { formatDateRange } from "little-date";
import PlusIcon from "@lucide/svelte/icons/plus";
import { Button } from "$lib/components/ui/button/index.js";
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import * as Card from "$lib/components/ui/card/index.js";
import { CalendarDate, getLocalTimeZone, type DateValue } from "@internationalized/date";
const events = [
{
title: "Team Sync Meeting",
start: "2025-06-12T09:00:00",
end: "2025-06-12T10:00:00",
},
{
title: "Design Review",
start: "2025-06-12T11:30:00",
end: "2025-06-12T12:30:00",
},
{
title: "Client Presentation",
start: "2025-06-12T14:00:00",
end: "2025-06-12T15:00:00",
},
];
let value = $state<DateValue | undefined>(new CalendarDate(2025, 6, 12));
</script>
<Card.Root class="w-fit py-4">
<Card.Content class="px-4">
<Calendar type="single" bind:value class="bg-transparent p-0" preventDeselect />
</Card.Content>
<Card.Footer class="flex flex-col items-start gap-3 border-t px-4 !pt-4">
<div class="flex w-full items-center justify-between px-1">
<div class="text-sm font-medium">
{value?.toDate(getLocalTimeZone()).toLocaleDateString("en-US", {
day: "numeric",
month: "long",
year: "numeric",
})}
</div>
<Button variant="ghost" size="icon" class="size-6" title="Add Event">
<PlusIcon />
<span class="sr-only">Add Event</span>
</Button>
</div>
<div class="flex w-full flex-col gap-2">
{#each events as event (event.title)}
<div
class="bg-muted after:bg-primary/70 relative rounded-md p-2 pl-6 text-sm after:absolute after:inset-y-2 after:left-2 after:w-1 after:rounded-full"
>
<div class="font-medium">{event.title}</div>
<div class="text-muted-foreground text-xs">
{formatDateRange(new Date(event.start), new Date(event.end))}
</div>
</div>
{/each}
</div>
</Card.Footer>
</Card.Root>
With event slots
calendar-31
Files
<script lang="ts">
import { getLocalTimeZone, type DateValue } from "@internationalized/date";
import CalendarPlusIcon from "@lucide/svelte/icons/calendar-plus";
import { Button } from "$lib/components/ui/button/index.js";
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import * as Drawer from "$lib/components/ui/drawer/index.js";
import { Label } from "$lib/components/ui/label/index.js";
let open = $state(false);
let value = $state<DateValue | undefined>();
const id = $props.id();
const triggerLabel = $derived.by(() => {
if (value) return value.toDate(getLocalTimeZone()).toLocaleDateString();
return "Select date";
});
</script>
<div class="flex flex-col gap-3">
<Label for="{id}-date" class="px-1">Date of birth</Label>
<Drawer.Root bind:open>
<Drawer.Trigger id="{id}-date">
{#snippet child({ props })}
<Button {...props} variant="outline" class="w-48 justify-between font-normal">
{triggerLabel}
<CalendarPlusIcon />
</Button>
{/snippet}
</Drawer.Trigger>
<Drawer.Content class="w-auto overflow-hidden p-0">
<Drawer.Header class="sr-only">
<Drawer.Title>Select date</Drawer.Title>
<Drawer.Description>Set your date of birth</Drawer.Description>
</Drawer.Header>
<Calendar
type="single"
bind:value
captionLayout="dropdown"
onValueChange={(v) => {
if (v) {
open = false;
}
}}
class="mx-auto [--cell-size:clamp(0px,calc(100vw/7.5),52px)]"
/>
</Drawer.Content>
</Drawer.Root>
<div class="text-muted-foreground px-1 text-sm">This example works best on mobile.</div>
</div>
Date picker in a drawer
calendar-32