Forms
Date range picker
Choose a start and an end day: two months at a glance, with quick picks for common spans.
Installation
npx sinaui add date-range-pickerCopy src/lib/ui/DateRangePicker.svelte, Calendar.svelte, Popover.svelte, floating.ts, announce.ts, Button.svelte, Icon.svelte and tokens.css, then install
its date library:
pnpm add @internationalized/dateThe value is { start, end }, two CalendarDates. For one day, use
the Date picker.
Examples
With quick picks
Choose the first day, then the last; the span follows the pointer in between.
Study leave
Study leave
October 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
November 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
No dates chosen yet.
<script lang="ts">
import { endOfMonth, getLocalTimeZone, startOfMonth, today } from '@internationalized/date';
import type { DateRange } from '#lib/ui/Calendar.svelte';
import DateRangePicker from '#lib/ui/DateRangePicker.svelte';
const now = today(getLocalTimeZone());
const next = startOfMonth(now).add({ months: 1 });
const presets = [
{ label: 'Next 7 days', start: now, end: now.add({ days: 6 }) },
{ label: 'Next 30 days', start: now, end: now.add({ days: 29 }) },
{ label: 'Rest of this month', start: now, end: endOfMonth(now) },
{ label: 'Next month', start: next, end: endOfMonth(next) }
];
let leave = $state<DateRange>();
const nights = $derived(leave && leave.end.compare(leave.start));
</script>
<div class="stack">
<DateRangePicker label="Study leave" bind:value={leave} {presets} min={now} />
<p>
{#if leave}{new Intl.PluralRules('en').select(nights!) === 'one'
? '1 night'
: `${nights} nights`} away from the House of Wisdom.{:else}No dates chosen yet.{/if}
</p>
</div>
<style>
.stack {
display: grid;
gap: 0.75rem;
inline-size: min(100%, 22rem);
}
p {
margin: 0;
color: var(--ui-muted);
font-size: 0.875rem;
font-variant-numeric: tabular-nums;
}
</style>Closed days
A range can't run across a day that's unavailable, so days beyond the next closed one can't end it.
Reading room booking
Reading room booking
October 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
November 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
The reading room is closed on Fridays.
On the page
The calendar on its own, in range mode, showing two months.
October 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
November 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
October 4 – 16, 2026
<script lang="ts">
import { getLocalTimeZone, today } from '@internationalized/date';
import Calendar, { type DateRange } from '#lib/ui/Calendar.svelte';
const start = today(getLocalTimeZone()).add({ days: 3 });
let voyage = $state<DateRange>({ start, end: start.add({ days: 12 }) });
const fmt = new Intl.DateTimeFormat('en', { dateStyle: 'long' });
const tz = getLocalTimeZone();
</script>
<div class="stack">
<Calendar label="Voyage from Basra" mode="range" months={2} bind:range={voyage} />
<p>{fmt.formatRange(voyage.start.toDate(tz), voyage.end.toDate(tz))}</p>
</div>
<style>
.stack {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.75rem;
}
p {
margin: 0;
color: var(--ui-muted);
font-size: 0.875rem;
font-variant-numeric: tabular-nums;
}
</style>Props
| Prop | Type | Default |
|---|---|---|
label | string (required) | |
value | { start, end }, bindable | |
presets | { label, start, end }[] | |
months | number | 2 |
min, max | CalendarDate | |
isUnavailable | (date) => boolean | |
startName, endName | string: hidden inputs for forms | |
locale | string | 'en' |
calendar | string | CustomCalendar | 'gregory' |
hint, error | string | |
disabled | boolean | false |
Keyboard
| Key | Does |
|---|---|
| Arrow keys | Move by day or week, across into the next month |
| Page Up / Page Down | Previous or next month (with Shift, year) |
| Enter / Space | Choose the start, then the end |
| Escape | Drop a half-chosen range; press again to close |
Accessibility
- Each month is a grid; every day in the range is marked selected.
- After the first day, screen readers hear that it’s the start and that the end comes next; once both are chosen, the whole range is read out.
- The popover opens on the chosen start day (or today), ready for the arrow keys.