Range Calendar
Two-click date range on bits-ui RangeCalendar. Cells between start and end fill as the selection grows — end-points round, inner days stay flush — so the span reads as one continuous ribbon.
Usage
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
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 |
range-calendar.svelte
<script lang="ts">
import { RangeCalendar } from '$lib/components/ui/range-calendar';
</script>
<RangeCalendar />Dropdown caption
Same range mechanics, swappable month + year selects for quick navigation.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
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 |
range-calendar.svelte
<RangeCalendar captionLayout="dropdown" />Keyboard navigation
Tab into the grid, arrow-keys move day by day, Enter sets the start date — keep arrowing and
press Enter again to set the end. Reduced-motion friendly: cells animate via CSS-var
transitions that zero out under prefers-reduced-motion: reduce.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
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 |
- ←/→
- Previous / next day
- ↑/↓
- Previous / next week
- PgUp / PgDn
- Previous / next month
- Enter
- Set start (first press), set end (second press)
range-calendar-keyboard.svelte
<!-- Tab into the calendar, then:
←/→ previous / next day
↑/↓ previous / next week
PgUp / PgDn previous / next month
Home / End start / end of week
Enter set start (first press) or end (second press)
Esc clear in-progress range -->
<RangeCalendar class="rounded-xl border border-border" />Source
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
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 |
range-calendar.svelte
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import * as RangeCalendar from './index.js';
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import type { ButtonVariant } from '$lib/components/ui/button/index.js';
import type { Snippet } from 'svelte';
import { isEqualMonth, type DateValue } from '@internationalized/date';
let {
ref = $bindable(null),
value = $bindable(),
placeholder = $bindable(),
weekdayFormat = 'short',
class: className,
buttonVariant = 'ghost',
captionLayout = 'label',
locale = 'en-US',
months: monthsProp,
years,
monthFormat: monthFormatProp,
yearFormat = 'numeric',
day,
disableDaysOutsideMonth = false,
...restProps
}: WithoutChildrenOrChild<RangeCalendarPrimitive.RootProps> & {
buttonVariant?: ButtonVariant;
captionLayout?: 'dropdown' | 'dropdown-months' | 'dropdown-years' | 'label';
months?: RangeCalendarPrimitive.MonthSelectProps['months'];
years?: RangeCalendarPrimitive.YearSelectProps['years'];
monthFormat?: RangeCalendarPrimitive.MonthSelectProps['monthFormat'];
yearFormat?: RangeCalendarPrimitive.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>
<RangeCalendarPrimitive.Root
bind:ref
bind:value
bind:placeholder
data-slot="range-calendar"
{weekdayFormat}
{disableDaysOutsideMonth}
class={cn(
'group/calendar bg-background p-3 p-3 [--cell-radius:var(--radius-lg)] [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
className
)}
{locale}
{monthFormat}
{yearFormat}
{...restProps}
>
{#snippet children({ months, weekdays })}
<RangeCalendar.Months>
<RangeCalendar.Nav>
<RangeCalendar.PrevButton variant={buttonVariant} />
<RangeCalendar.NextButton variant={buttonVariant} />
</RangeCalendar.Nav>
{#each months as month, monthIndex (month)}
<RangeCalendar.Month>
<RangeCalendar.Header>
<RangeCalendar.Caption
{captionLayout}
months={monthsProp}
{monthFormat}
{years}
{yearFormat}
month={month.value}
bind:placeholder
{locale}
{monthIndex}
/>
</RangeCalendar.Header>
<RangeCalendar.Grid>
<RangeCalendar.GridHead>
<RangeCalendar.GridRow class="select-none">
{#each weekdays as weekday, i (i)}
<RangeCalendar.HeadCell>
{weekday.slice(0, 2)}
</RangeCalendar.HeadCell>
{/each}
</RangeCalendar.GridRow>
</RangeCalendar.GridHead>
<RangeCalendar.GridBody>
{#each month.weeks as weekDates (weekDates)}
<RangeCalendar.GridRow class="mt-2 w-full">
{#each weekDates as date (date)}
<RangeCalendar.Cell {date} month={month.value}>
{#if day}
{@render day({
day: date,
outsideMonth: !isEqualMonth(date, month.value)
})}
{:else}
<RangeCalendar.Day />
{/if}
</RangeCalendar.Cell>
{/each}
</RangeCalendar.GridRow>
{/each}
</RangeCalendar.GridBody>
</RangeCalendar.Grid>
</RangeCalendar.Month>
{/each}
</RangeCalendar.Months>
{/snippet}
</RangeCalendarPrimitive.Root>
RangeCalendar props
Inherits bits-ui RangeCalendar.Root props via spread. Cells between the start and end dates render the `range-fill` state; end-points carry `range-start` / `range-end`.
| Prop | Type | Default | Description |
|---|---|---|---|
{ start?: DateValue; end?: DateValue } (bindable) | — | The selected range. `start` is set on first click, `end` on the second. | |
DateValue (bindable) | — | The visible month anchor when the range is empty. | |
'label' | 'dropdown' | 'dropdown-months' | 'dropdown-years' | 'label' | Heading style — plain label, full dropdown, or one of each. | |
ButtonVariant | 'ghost' | Variant applied to the Prev/Next nav buttons. | |
string | 'en-US' | BCP-47 tag used for weekday names, month labels, and number formatting. | |
'narrow' | 'short' | 'long' | 'short' | Intl.DateTimeFormat option for the weekday header row. | |
boolean | false | When true, adjacent-month days render but are not interactive. | |
Snippet<[{ day: DateValue; outsideMonth: boolean }]> | — | Optional render override for a single cell. Falls back to `RangeCalendar.Day`. | |
string | — | Merged onto the root container via tailwind-merge. |