Date Range Field
Dates & TimeLocal guide ↗
import { DateRangeField } from "bits-ui" Bound stay duration
The start and end segments share one bound range; external controls preserve its duration
Stay dates
09102026
09142026
4 nights
<script lang="ts">
import { DateRangeField, type DateRange } from 'bits-ui';
import { getLocalTimeZone, today } from '@internationalized/date';
const initial = today(getLocalTimeZone());
let value = $state<DateRange>({
start: initial.add({ days: 10 }),
end: initial.add({ days: 14 })
});
const nights = $derived(
value.start && value.end ? Math.max(0, value.end.compare(value.start)) : 0
);
function shiftRange(days: number) {
if (!value.start || !value.end) return;
value = {
start: value.start.add({ days }),
end: value.end.add({ days })
};
}
</script>
<div class="mx-auto grid w-full max-w-xl gap-4">
<DateRangeField.Root bind:value class="group grid gap-2">
<DateRangeField.Label class="text-sm font-medium text-ink-200">Stay dates</DateRangeField.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)}
<DateRangeField.Input {type} name={`stay-${type}`} class="flex min-w-0 items-center">
{#snippet children({ segments })}
{#each segments as { part, value: segmentValue }, i (part + i)}
{#if part === 'literal'}
<DateRangeField.Segment {part} class="px-0.5 text-ink-600">
{segmentValue}
</DateRangeField.Segment>
{:else}
<DateRangeField.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}
</DateRangeField.Segment>
{/if}
{/each}
{/snippet}
</DateRangeField.Input>
{#if type === 'start'}
<span aria-hidden="true" class="px-1 text-ink-600">–</span>
{/if}
{/each}
</div>
</DateRangeField.Root>
<div class="flex flex-wrap items-center gap-2">
<button type="button" class="btn btn-sm" onclick={() => shiftRange(-1)}>Previous day</button>
<button type="button" class="btn btn-sm" onclick={() => shiftRange(1)}>Next day</button>
<span class="chip ml-auto">{nights} {nights === 1 ? 'night' : 'nights'}</span>
</div>
</div>
Date and time range
A CalendarDateTime range rendered to minute granularity with an explicit 24-hour cycle
Studio booking window
180920261630
180920261800
2026-09-18T16:30:00 → 2026-09-18T18:00:00<script lang="ts">
import { DateRangeField, type DateRange } from 'bits-ui';
import { CalendarDateTime } from '@internationalized/date';
let value = $state<DateRange>({
start: new CalendarDateTime(2026, 9, 18, 16, 30),
end: new CalendarDateTime(2026, 9, 18, 18, 0)
});
function moveWindow(hours: number) {
if (!value.start || !value.end) return;
value = {
start: value.start.add({ hours }),
end: value.end.add({ hours })
};
}
</script>
<div class="mx-auto grid w-full max-w-2xl gap-4">
<DateRangeField.Root
bind:value
granularity="minute"
hourCycle={24}
locale="en-GB"
class="group grid gap-2"
>
<DateRangeField.Label class="text-sm font-medium text-ink-200">
Studio booking window
</DateRangeField.Label>
<div
class="input flex min-h-11 items-center gap-1 overflow-x-auto group-data-invalid:border-accent-500 focus-within:border-brand-600"
>
{#each ['start', 'end'] as const as type (type)}
<DateRangeField.Input {type} class="flex shrink-0 items-center">
{#snippet children({ segments })}
{#each segments as { part, value: segmentValue }, i (part + i)}
{#if part === 'literal'}
<DateRangeField.Segment {part} class="px-0.5 text-ink-600">
{segmentValue}
</DateRangeField.Segment>
{:else}
<DateRangeField.Segment
{part}
class="rounded-md px-1 py-0.5 font-mono text-xs tabular-nums outline-none hover:bg-ink-850 focus:bg-brand-500 focus:text-ink-950 aria-[valuetext=Empty]:text-ink-600"
>
{segmentValue}
</DateRangeField.Segment>
{/if}
{/each}
{/snippet}
</DateRangeField.Input>
{#if type === 'start'}
<span aria-hidden="true" class="px-1 text-xs text-ink-600">to</span>
{/if}
{/each}
</div>
</DateRangeField.Root>
<div class="flex flex-wrap items-center gap-2">
<button type="button" class="btn btn-sm" onclick={() => moveWindow(-1)}>−1 hour</button>
<button type="button" class="btn btn-sm" onclick={() => moveWindow(1)}>+1 hour</button>
{#if value.start && value.end}
<code class="muted ml-auto text-xs">{value.start.toString()} → {value.end.toString()}</code>
{/if}
</div>
</div>
Each DateRangeField.Input requires a type of start or end; its optional name creates a hidden form input. The bound value is one object with start and end date values, and either side may be temporarily undefined while the user is entering it.
Granularity defaults to day for a CalendarDate and minute for CalendarDateTime. It controls which segments render, while locale controls their order and separators; hourCycle overrides the locale's 12- or 24-hour preference when time segments are present.