Date Range Picker
Dates & Timeimport { DateRangePicker } from "bits-ui" Presets and range calendar
Last 7 days and this month replace the same bound range edited by the field and calendar
Bound range: 2026-08-25 — 2026-08-31
<script lang="ts">
import { DateRangePicker, Portal, type DateRange } from 'bits-ui';
import { CalendarDays, ChevronLeft, ChevronRight } from '@lucide/svelte';
import { getLocalTimeZone, today } from '@internationalized/date';
const initial = today(getLocalTimeZone());
let placeholder = $state(initial);
let value = $state<DateRange>({
start: initial.subtract({ days: 6 }),
end: initial
});
const summary = $derived(
value.start && value.end
? `${value.start.toString()} — ${value.end.toString()}`
: 'Choose both dates'
);
function selectLastSevenDays() {
const end = today(getLocalTimeZone());
const start = end.subtract({ days: 6 });
value = { start, end };
placeholder = start;
}
function selectThisMonth() {
const start = today(getLocalTimeZone()).set({ day: 1 });
const end = start.add({ months: 1 }).subtract({ days: 1 });
value = { start, end };
placeholder = start;
}
</script>
<div class="mx-auto grid w-full max-w-xl gap-3">
<DateRangePicker.Root
bind:value
bind:placeholder
weekdayFormat="short"
fixedWeeks
calendarLabel="Travel date range"
class="group grid gap-2"
>
<DateRangePicker.Label class="text-sm font-medium text-ink-200">Travel dates</DateRangePicker.Label>
<div
class="input flex min-h-11 items-center gap-1 group-data-invalid:border-accent-500 focus-within:border-brand-600"
>
{#each ['start', 'end'] as const as type (type)}
<DateRangePicker.Input {type} name={`travel-${type}`} class="flex min-w-0 items-center">
{#snippet children({ segments })}
{#each segments as { part, value: segmentValue }, i (part + i)}
{#if part === 'literal'}
<DateRangePicker.Segment {part} class="px-0.5 text-ink-600">
{segmentValue}
</DateRangePicker.Segment>
{:else}
<DateRangePicker.Segment
{part}
class="rounded-md px-1 py-0.5 font-mono text-sm tabular-nums outline-none hover:bg-ink-850 focus:bg-brand-500 focus:text-ink-950 aria-[valuetext=Empty]:text-ink-600"
>
{segmentValue}
</DateRangePicker.Segment>
{/if}
{/each}
{/snippet}
</DateRangePicker.Input>
{#if type === 'start'}
<span aria-hidden="true" class="px-1 text-ink-600">–</span>
{/if}
{/each}
<DateRangePicker.Trigger
class="ml-auto grid size-8 shrink-0 place-items-center rounded-md text-ink-400 outline-none hover:bg-ink-850 hover:text-ink-50 focus-visible:ring-2 focus-visible:ring-brand-500"
aria-label="Open travel calendar"
>
<CalendarDays class="size-4" />
</DateRangePicker.Trigger>
</div>
<Portal>
<DateRangePicker.Content
align="start"
sideOffset={8}
class="panel z-50 w-[min(22rem,calc(100vw-2rem))] p-3 text-ink-50 animate-pop-in"
>
<div class="mb-3 flex flex-wrap gap-2 border-b border-ink-800 pb-3">
<button type="button" class="btn btn-sm" onclick={selectLastSevenDays}>Last 7 days</button>
<button type="button" class="btn btn-sm" onclick={selectThisMonth}>This month</button>
</div>
<DateRangePicker.Calendar>
{#snippet children({ months, weekdays })}
<DateRangePicker.Header class="mb-3 flex items-center justify-between">
<DateRangePicker.PrevButton class="btn btn-ghost btn-sm size-8 p-0" aria-label="Previous month">
<ChevronLeft class="size-4" />
</DateRangePicker.PrevButton>
<DateRangePicker.Heading class="text-sm font-semibold" />
<DateRangePicker.NextButton class="btn btn-ghost btn-sm size-8 p-0" aria-label="Next month">
<ChevronRight class="size-4" />
</DateRangePicker.NextButton>
</DateRangePicker.Header>
{#each months as month (month.value.toString())}
<DateRangePicker.Grid class="w-full border-collapse">
<DateRangePicker.GridHead>
<DateRangePicker.GridRow>
{#each weekdays as day (day)}
<DateRangePicker.HeadCell class="h-8 text-center text-[0.7rem] font-medium text-ink-600">
{day.slice(0, 2)}
</DateRangePicker.HeadCell>
{/each}
</DateRangePicker.GridRow>
</DateRangePicker.GridHead>
<DateRangePicker.GridBody>
{#each month.weeks as weekDates (weekDates[0].toString())}
<DateRangePicker.GridRow>
{#each weekDates as date (date.toString())}
<DateRangePicker.Cell {date} month={month.value} class="p-0 text-center">
<DateRangePicker.Day
class="grid size-9 place-items-center rounded-md text-xs text-ink-200 outline-none hover:bg-ink-800 focus-visible:ring-2 focus-visible:ring-brand-500 data-today:ring-1 data-today:ring-ink-600 data-range-middle:rounded-none data-range-middle:bg-brand-500/20 data-range-middle:text-brand-400 data-range-start:bg-brand-500 data-range-start:text-ink-950 data-range-end:bg-brand-500 data-range-end:text-ink-950 data-disabled:pointer-events-none data-disabled:opacity-30 data-unavailable:line-through data-outside-month:text-ink-600"
>
{date.day}
</DateRangePicker.Day>
</DateRangePicker.Cell>
{/each}
</DateRangePicker.GridRow>
{/each}
</DateRangePicker.GridBody>
</DateRangePicker.Grid>
{/each}
{/snippet}
</DateRangePicker.Calendar>
</DateRangePicker.Content>
</Portal>
</DateRangePicker.Root>
<p class="muted text-xs">Bound range: <span class="font-mono text-ink-200">{summary}</span></p>
</div>
Constrained stay
Minimum and maximum range lengths with the popover held open while dates are refined
Production reservation
Select 3–10 consecutive calendar days
<script lang="ts">
import { DateRangePicker, Portal, type DateRange } from 'bits-ui';
import { CalendarDays, ChevronLeft, ChevronRight } from '@lucide/svelte';
import { getLocalTimeZone, today } from '@internationalized/date';
const initial = today(getLocalTimeZone());
let value = $state<DateRange>({
start: initial.add({ days: 7 }),
end: initial.add({ days: 11 })
});
let placeholder = $state(initial.add({ days: 7 }));
const selectedDays = $derived(
value.start && value.end ? value.end.compare(value.start) + 1 : undefined
);
</script>
<div class="mx-auto grid w-full max-w-xl gap-3">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-medium text-ink-200">Production reservation</p>
<p class="muted text-xs">Select 3–10 consecutive calendar days</p>
</div>
{#if selectedDays}<span class="chip">{selectedDays} days</span>{/if}
</div>
<DateRangePicker.Root
bind:value
bind:placeholder
minDays={3}
maxDays={10}
closeOnRangeSelect={false}
weekdayFormat="short"
fixedWeeks
calendarLabel="Production reservation dates"
class="group"
>
<div
class="input flex min-h-11 items-center gap-1 group-data-invalid:border-accent-500 focus-within:border-brand-600"
>
{#each ['start', 'end'] as const as type (type)}
<DateRangePicker.Input {type} class="flex min-w-0 items-center">
{#snippet children({ segments })}
{#each segments as { part, value: segmentValue }, i (part + i)}
{#if part === 'literal'}
<DateRangePicker.Segment {part} class="px-0.5 text-ink-600">
{segmentValue}
</DateRangePicker.Segment>
{:else}
<DateRangePicker.Segment
{part}
class="rounded-md px-1 py-0.5 font-mono text-sm tabular-nums outline-none hover:bg-ink-850 focus:bg-brand-500 focus:text-ink-950 aria-[valuetext=Empty]:text-ink-600"
>
{segmentValue}
</DateRangePicker.Segment>
{/if}
{/each}
{/snippet}
</DateRangePicker.Input>
{#if type === 'start'}<span aria-hidden="true" class="px-1 text-ink-600">–</span>{/if}
{/each}
<DateRangePicker.Trigger
class="ml-auto grid size-8 shrink-0 place-items-center rounded-md text-ink-400 outline-none hover:bg-ink-850 hover:text-ink-50 focus-visible:ring-2 focus-visible:ring-brand-500"
aria-label="Open reservation calendar"
>
<CalendarDays class="size-4" />
</DateRangePicker.Trigger>
</div>
<Portal>
<DateRangePicker.Content
align="start"
sideOffset={8}
class="panel z-50 w-[min(22rem,calc(100vw-2rem))] p-3 text-ink-50 animate-pop-in"
>
<DateRangePicker.Calendar>
{#snippet children({ months, weekdays })}
<DateRangePicker.Header class="mb-3 flex items-center justify-between">
<DateRangePicker.PrevButton class="btn btn-ghost btn-sm size-8 p-0" aria-label="Previous month">
<ChevronLeft class="size-4" />
</DateRangePicker.PrevButton>
<div class="text-center">
<DateRangePicker.Heading class="text-sm font-semibold" />
<p class="muted text-[0.65rem]">Popover stays open while refining the range</p>
</div>
<DateRangePicker.NextButton class="btn btn-ghost btn-sm size-8 p-0" aria-label="Next month">
<ChevronRight class="size-4" />
</DateRangePicker.NextButton>
</DateRangePicker.Header>
{#each months as month (month.value.toString())}
<DateRangePicker.Grid class="w-full border-collapse">
<DateRangePicker.GridHead>
<DateRangePicker.GridRow>
{#each weekdays as day (day)}
<DateRangePicker.HeadCell class="h-8 text-center text-[0.7rem] font-medium text-ink-600">
{day.slice(0, 2)}
</DateRangePicker.HeadCell>
{/each}
</DateRangePicker.GridRow>
</DateRangePicker.GridHead>
<DateRangePicker.GridBody>
{#each month.weeks as weekDates (weekDates[0].toString())}
<DateRangePicker.GridRow>
{#each weekDates as date (date.toString())}
<DateRangePicker.Cell {date} month={month.value} class="p-0 text-center">
<DateRangePicker.Day
class="grid size-9 place-items-center rounded-md text-xs text-ink-200 outline-none hover:bg-ink-800 focus-visible:ring-2 focus-visible:ring-brand-500 data-today:ring-1 data-today:ring-ink-600 data-range-middle:rounded-none data-range-middle:bg-brand-500/20 data-range-middle:text-brand-400 data-range-start:bg-brand-500 data-range-start:text-ink-950 data-range-end:bg-brand-500 data-range-end:text-ink-950 data-disabled:pointer-events-none data-disabled:opacity-30 data-outside-month:text-ink-600"
>
{date.day}
</DateRangePicker.Day>
</DateRangePicker.Cell>
{/each}
</DateRangePicker.GridRow>
{/each}
</DateRangePicker.GridBody>
</DateRangePicker.Grid>
{/each}
{/snippet}
</DateRangePicker.Calendar>
</DateRangePicker.Content>
</Portal>
</DateRangePicker.Root>
</div>
Date Range Picker composes Date Range Field, Range Calendar, and Popover state. Its value is still one { start, end } object; presets must replace those immutable date values rather than mutate them. Selection closes the popover by default, while closeOnRangeSelect can keep it open.
The top-level Portal utility renders the picker content into document.body by default and accepts a custom to target. Locale controls field formatting and the calendar's first weekday unless weekStartsOn is set; granularity and hourCycle follow the same rules as Date Range Field when the bound values include time.