Calendar
Dates & Timeimport { Calendar } from "bits-ui" Single date
type="single" binds one DateValue while the grid exposes selection and today states
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 31 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 1 | 2 | 3 | 4 | 5 |
<script lang="ts">
import { Calendar } from 'bits-ui';
import {
CalendarDate,
DateFormatter,
getLocalTimeZone,
today,
type DateValue
} from '@internationalized/date';
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
const timeZone = getLocalTimeZone();
const current = today(timeZone);
const placeholder = new CalendarDate(current.year, current.month, 1);
const formatter = new DateFormatter('en-US', { dateStyle: 'full' });
let value = $state<DateValue | undefined>(current);
const formattedValue = $derived(
value ? formatter.format(value.toDate(timeZone)) : 'No date selected'
);
</script>
<div class="grid justify-items-center gap-4">
<div class="flex w-full max-w-sm items-center justify-between gap-4 rounded-lg border border-ink-800 bg-ink-950 px-3 py-2">
<span class="muted text-xs">Selected date</span>
<output class="text-right text-sm font-medium text-ink-50">{formattedValue}</output>
</div>
<Calendar.Root
type="single"
bind:value
{placeholder}
calendarLabel="Choose a date"
weekdayFormat="short"
class="panel w-full max-w-sm p-4"
>
{#snippet children({ months, weekdays })}
<Calendar.Header class="mb-4 flex items-center justify-between">
<Calendar.PrevButton class="btn btn-ghost btn-sm size-9 p-0" aria-label="Previous month">
<ChevronLeft class="size-4" aria-hidden="true" />
</Calendar.PrevButton>
<Calendar.Heading class="text-sm font-semibold text-ink-50" />
<Calendar.NextButton class="btn btn-ghost btn-sm size-9 p-0" aria-label="Next month">
<ChevronRight class="size-4" aria-hidden="true" />
</Calendar.NextButton>
</Calendar.Header>
{#each months as month (month.value.toString())}
<Calendar.Grid class="w-full table-fixed border-collapse">
<Calendar.GridHead>
<Calendar.GridRow>
{#each weekdays as day}
<Calendar.HeadCell class="h-8 text-center text-[0.7rem] font-medium text-ink-600">
{day}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates, weekIndex (weekIndex)}
<Calendar.GridRow>
{#each weekDates as date (date.toString())}
<Calendar.Cell {date} month={month.value} class="p-0 text-center">
<Calendar.Day
class="inline-flex size-9 items-center justify-center rounded-lg text-sm text-ink-200 outline-none transition hover:bg-ink-800 focus-visible:ring-2 focus-visible:ring-brand-500 data-[disabled]:pointer-events-none data-[disabled]:opacity-30 data-[outside-month]:text-ink-600 data-[selected]:bg-brand-500 data-[selected]:font-semibold data-[selected]:text-ink-950 data-[today]:ring-1 data-[today]:ring-brand-400"
/>
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
{/each}
{/snippet}
</Calendar.Root>
</div>
Two-month planning
numberOfMonths renders adjacent grids; pagedNavigation advances them as one view
Roadmap planning
Navigation advances by both visible months.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 31 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 1 | 2 | 3 | 4 | 5 |
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
<script lang="ts">
import { Calendar } from 'bits-ui';
import {
CalendarDate,
DateFormatter,
getLocalTimeZone,
today,
type DateValue
} from '@internationalized/date';
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
const timeZone = getLocalTimeZone();
const current = today(timeZone);
const placeholder = new CalendarDate(current.year, current.month, 1);
const formatter = new DateFormatter('en-US', { dateStyle: 'medium' });
let value = $state<DateValue | undefined>(current.add({ days: 10 }));
const formattedValue = $derived(
value ? formatter.format(value.toDate(timeZone)) : 'Choose a planning date'
);
</script>
<div class="grid gap-4">
<div class="flex flex-wrap items-center justify-between gap-2">
<div>
<p class="text-sm font-medium text-ink-50">Roadmap planning</p>
<p class="muted text-xs">Navigation advances by both visible months.</p>
</div>
<span class="chip font-mono">{formattedValue}</span>
</div>
<Calendar.Root
type="single"
bind:value
{placeholder}
numberOfMonths={2}
pagedNavigation
fixedWeeks
calendarLabel="Choose a roadmap date"
class="panel w-full p-4"
>
{#snippet children({ months, weekdays })}
<Calendar.Header class="mb-4 flex items-center justify-between">
<Calendar.PrevButton class="btn btn-ghost btn-sm size-9 p-0" aria-label="Previous two months">
<ChevronLeft class="size-4" aria-hidden="true" />
</Calendar.PrevButton>
<Calendar.Heading class="text-sm font-semibold text-ink-50" />
<Calendar.NextButton class="btn btn-ghost btn-sm size-9 p-0" aria-label="Next two months">
<ChevronRight class="size-4" aria-hidden="true" />
</Calendar.NextButton>
</Calendar.Header>
<div class="grid gap-6 sm:grid-cols-2">
{#each months as month (month.value.toString())}
<Calendar.Grid class="w-full table-fixed border-collapse">
<Calendar.GridHead>
<Calendar.GridRow>
{#each weekdays as day}
<Calendar.HeadCell class="h-7 text-center text-[0.65rem] font-medium text-ink-600">
{day}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates, weekIndex (weekIndex)}
<Calendar.GridRow>
{#each weekDates as date (date.toString())}
<Calendar.Cell {date} month={month.value} class="p-0 text-center">
<Calendar.Day
class="inline-flex size-8 items-center justify-center rounded-md text-xs text-ink-200 outline-none transition hover:bg-ink-800 focus-visible:ring-2 focus-visible:ring-brand-500 data-[disabled]:pointer-events-none data-[disabled]:opacity-30 data-[outside-month]:text-ink-700 data-[selected]:bg-brand-500 data-[selected]:font-semibold data-[selected]:text-ink-950 data-[today]:ring-1 data-[today]:ring-brand-400"
/>
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
{/each}
</div>
{/snippet}
</Calendar.Root>
</div>
Bounded availability
minValue and maxValue bound the window while isDateUnavailable marks individual dates
Installation window
Sep 7
Book from Aug 31 through Sep 21.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 1 | 2 | 3 |
<script lang="ts">
import { Calendar } from 'bits-ui';
import {
CalendarDate,
DateFormatter,
getLocalTimeZone,
today,
type DateValue
} from '@internationalized/date';
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
const timeZone = getLocalTimeZone();
const current = today(timeZone);
const initial = current.add({ days: 7 });
const placeholder = new CalendarDate(initial.year, initial.month, 1);
const minValue: DateValue = current;
const maxValue: DateValue = current.add({ days: 21 });
const unavailableDates = new Set([
current.add({ days: 9 }).toString(),
current.add({ days: 13 }).toString(),
current.add({ days: 17 }).toString()
]);
const formatter = new DateFormatter('en-US', { month: 'short', day: 'numeric' });
let value = $state<DateValue | undefined>(initial);
function isDateUnavailable(date: DateValue) {
return unavailableDates.has(date.toString());
}
function formatDate(date: DateValue | undefined) {
return date ? formatter.format(date.toDate(timeZone)) : 'None';
}
</script>
<div class="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-start">
<div class="grid gap-3">
<div class="card p-4">
<p class="muted text-xs font-medium tracking-wider uppercase">Installation window</p>
<p class="mt-1 text-lg font-semibold text-ink-50">{formatDate(value)}</p>
<p class="muted mt-1 text-xs">
Book from {formatDate(minValue)} through {formatDate(maxValue)}.
</p>
</div>
<div class="flex flex-wrap gap-2 text-xs">
<span class="chip"><span class="size-2 rounded-full bg-brand-500"></span>Selected</span>
<span class="chip"><span class="size-2 rounded-full bg-accent-500"></span>Unavailable</span>
<span class="chip"><span class="size-2 rounded-full bg-ink-600"></span>Out of window</span>
</div>
</div>
<Calendar.Root
type="single"
bind:value
{placeholder}
{minValue}
{maxValue}
{isDateUnavailable}
calendarLabel="Choose an installation date"
class="panel w-full min-w-72 p-4"
>
{#snippet children({ months, weekdays })}
<Calendar.Header class="mb-4 flex items-center justify-between">
<Calendar.PrevButton class="btn btn-ghost btn-sm size-9 p-0" aria-label="Previous month">
<ChevronLeft class="size-4" aria-hidden="true" />
</Calendar.PrevButton>
<Calendar.Heading class="text-sm font-semibold text-ink-50" />
<Calendar.NextButton class="btn btn-ghost btn-sm size-9 p-0" aria-label="Next month">
<ChevronRight class="size-4" aria-hidden="true" />
</Calendar.NextButton>
</Calendar.Header>
{#each months as month (month.value.toString())}
<Calendar.Grid class="w-full table-fixed border-collapse">
<Calendar.GridHead>
<Calendar.GridRow>
{#each weekdays as day}
<Calendar.HeadCell class="h-7 text-center text-[0.65rem] font-medium text-ink-600">
{day}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates, weekIndex (weekIndex)}
<Calendar.GridRow>
{#each weekDates as date (date.toString())}
<Calendar.Cell {date} month={month.value} class="p-0 text-center">
<Calendar.Day
class="inline-flex size-8 items-center justify-center rounded-md text-xs text-ink-200 outline-none transition hover:bg-ink-800 focus-visible:ring-2 focus-visible:ring-brand-500 data-[disabled]:pointer-events-none data-[disabled]:text-ink-700 data-[outside-month]:text-ink-700 data-[selected]:bg-brand-500 data-[selected]:font-semibold data-[selected]:text-ink-950 data-[unavailable]:text-accent-500 data-[unavailable]:line-through data-[today]:ring-1 data-[today]:ring-brand-400"
/>
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
{/each}
{/snippet}
</Calendar.Root>
</div>
Calendar state uses immutable DateValue objects from @internationalized/date, not native Date instances. The required type prop also changes the value shape: "single" uses one value, while "multiple" uses an array.
placeholder is navigation state: it chooses the visible month when no selection exists and updates as the user pages. It is independent from value, which is the actual selection. Convert to a native date with an explicit time zone only at the formatting or integration boundary.