Date Picker
Popover-anchored calendar with single-date and range variants. Wraps Calendar + Popover — the trigger is a Button that formats the selection with Intl DateFormatter.
Single
<script lang="ts">
import { DatePicker } from '$lib/components/ui/date-picker';
import type { DateValue } from '@internationalized/date';
let value = $state<DateValue | undefined>(undefined);
</script>
<DatePicker bind:value />Range
<script lang="ts">
import { DateRangePicker, type DateRange } from '$lib/components/ui/date-picker';
let value = $state<DateRange>({ start: undefined, end: undefined });
</script>
<DateRangePicker bind:value />Disabled
<script lang="ts" module>
import type { DateValue } from '@internationalized/date';
import type { WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
export type DateRange = { start: DateValue | undefined; end: DateValue | undefined };
export type DatePickerProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
value?: DateValue | undefined;
open?: boolean;
placeholder?: string;
disabled?: boolean;
locale?: string;
triggerClass?: string;
contentClass?: string;
};
</script>
<script lang="ts">
import { DateFormatter } from '@internationalized/date';
import { Calendar } from '$lib/components/ui/calendar';
import * as Popover from '$lib/components/ui/popover';
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils.js';
import { Icon } from '$lib/components/ui/icon';
let {
value = $bindable(undefined),
open = $bindable(false),
placeholder = 'Pick a date',
disabled = false,
locale = 'en-US',
class: className,
triggerClass,
contentClass,
ref = $bindable(null)
}: DatePickerProps = $props();
const formatter = $derived(new DateFormatter(locale, { dateStyle: 'long' }));
const label = $derived(value ? formatter.format(value.toDate('UTC')) : placeholder);
</script>
<div bind:this={ref} data-slot="date-picker" class={cn('relative', className)}>
<Popover.Root bind:open>
<Popover.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="outline"
{disabled}
class={cn('w-56 justify-start gap-2 font-normal', triggerClass)}
>
<Icon name="calendar-line" size="1rem" class="size-4 opacity-60" aria-hidden="true" />
<span class={cn(!value && 'text-muted-foreground')}>{label}</span>
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class={cn('w-auto p-0', contentClass)}>
<Calendar type="single" bind:value {locale} />
</Popover.Content>
</Popover.Root>
</div>
Date of Birth Picker
Popover-anchored calendar with month + year dropdowns. The caption layout lets users reach a birth year in two clicks instead of navigating month by month.
<script lang="ts">
import { Calendar } from '$lib/components/ui/calendar';
import * as Popover from '$lib/components/ui/popover';
import { Button } from '$lib/components/ui/button';
import { DateFormatter, type DateValue } from '@internationalized/date';
import { Icon } from '$lib/components/ui/icon';
const fmt = new DateFormatter('en-US', { dateStyle: 'long' });
let value = $state<DateValue | undefined>(undefined);
let open = $state(false);
</script>
<Popover.Root bind:open>
<Popover.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline" class="w-64 justify-start gap-2 font-normal">
<Icon name="calendar-line" class="size-4 opacity-60" size="1rem" aria-hidden="true" />
<span class={!value ? 'text-muted-foreground' : ''}>
{value ? fmt.format(value.toDate('UTC')) : 'Date of birth'}
</span>
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto p-0">
<Calendar
type="single"
captionLayout="dropdown"
bind:value
onValueChange={() => (open = false)}
/>
</Popover.Content>
</Popover.Root>Picker with Input
A text field accepts ISO dates (YYYY-MM-DD) and stays in sync
with the calendar popover. Clicking a day backfills the input; typing a valid date jumps the
calendar.
<script lang="ts">
import { Calendar } from '$lib/components/ui/calendar';
import * as Popover from '$lib/components/ui/popover';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { CalendarDate, type DateValue } from '@internationalized/date';
import { Icon } from '$lib/components/ui/icon';
let text = $state('');
let value = $state<DateValue | undefined>(undefined);
let open = $state(false);
let error = $state('');
function parseIso(s: string): DateValue | undefined {
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!m) return undefined;
try { return new CalendarDate(+m[1], +m[2], +m[3]); } catch { return undefined; }
}
function onInput(e: Event) {
text = (e.target as HTMLInputElement).value;
const parsed = parseIso(text);
error = text && !parsed ? 'Use YYYY-MM-DD' : '';
value = parsed;
}
function onCalendarSelect(d: DateValue | undefined) {
value = d;
if (d) text = `${d.year}-${String(d.month).padStart(2,'0')}-${String(d.day).padStart(2,'0')}`;
open = false;
}
</script>
<div class="flex w-64 flex-col gap-2">
<div class="relative">
<Input placeholder="YYYY-MM-DD" oninput={onInput} value={text} class="pr-10" />
<Popover.Root bind:open>
<Popover.Trigger class="absolute right-2 top-1/2 -translate-y-1/2">
<Icon name="calendar-line" class="size-4 text-muted-foreground" size="1rem" aria-hidden="true" />
</Popover.Trigger>
<Popover.Content class="w-auto p-0">
<Calendar
type="single"
bind:value
onValueChange={onCalendarSelect}
/>
</Popover.Content>
</Popover.Root>
</div>
{#if error}<p class="text-xs text-destructive">{error}</p>{/if}
</div>Date and Time Picker
A popover-anchored calendar paired with a time input. Date and time values remain independent — combine them at submit time.
<script lang="ts">
import { Calendar } from '$lib/components/ui/calendar';
import * as Popover from '$lib/components/ui/popover';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { DateFormatter, type DateValue } from '@internationalized/date';
import { Icon } from '$lib/components/ui/icon';
const fmt = new DateFormatter('en-US', { dateStyle: 'medium' });
let dateValue = $state<DateValue | undefined>(undefined);
let open = $state(false);
let time = $state('12:00');
</script>
<div class="flex flex-col gap-2">
<Popover.Root bind:open>
<Popover.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline" class="w-64 justify-start gap-2 font-normal">
<Icon name="calendar-line" class="size-4 opacity-60" size="1rem" aria-hidden="true" />
<span class={!dateValue ? 'text-muted-foreground' : ''}>
{dateValue ? fmt.format(dateValue.toDate('UTC')) : 'Pick a date'}
</span>
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto p-0">
<Calendar
type="single"
bind:value={dateValue}
onValueChange={() => (open = false)}
/>
</Popover.Content>
</Popover.Root>
<div class="flex items-center gap-2">
<span class="w-10 text-sm text-muted-foreground">Time</span>
<Input type="time" bind:value={time} class="w-36" />
</div>
</div>Natural Language Picker
A text input parses phrases like today, tomorrow, in 3 days and reflects the result in a calendar popover. The regex parser here demonstrates the pattern — swap for chrono-node in production.
<script lang="ts">
import { Calendar } from '$lib/components/ui/calendar';
import * as Popover from '$lib/components/ui/popover';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { today, getLocalTimeZone, DateFormatter, type DateValue } from '@internationalized/date';
import { Icon } from '$lib/components/ui/icon';
const fmt = new DateFormatter('en-US', { dateStyle: 'long' });
let text = $state('');
let value = $state<DateValue | undefined>(undefined);
let open = $state(false);
let error = $state('');
function parse(s: string): DateValue | undefined {
const t = s.trim().toLowerCase();
const base = today(getLocalTimeZone());
if (!t) return undefined;
if (t === 'today') return base;
if (t === 'tomorrow') return base.add({ days: 1 });
const m = t.match(/^in\s+(\d+)\s+days?$/);
if (m) return base.add({ days: parseInt(m[1]) });
return undefined;
}
function onInput(e: Event) {
text = (e.target as HTMLInputElement).value;
const result = parse(text);
error = text && !result ? 'Try: today · tomorrow · in 3 days' : '';
value = result;
}
</script>
<div class="flex flex-col gap-2">
<div class="relative">
<Input
placeholder="today · tomorrow · in 3 days"
oninput={onInput}
value={text}
class="pr-10"
/>
<Popover.Root bind:open>
<Popover.Trigger class="absolute right-2 top-1/2 -translate-y-1/2">
<Icon name="calendar-line" class="size-4 text-muted-foreground" size="1rem" aria-hidden="true" />
</Popover.Trigger>
<Popover.Content class="w-auto p-0">
<Calendar type="single" bind:value onValueChange={() => (open = false)} />
</Popover.Content>
</Popover.Root>
</div>
{#if error}<p class="text-xs text-destructive">{error}</p>{/if}
{#if value}
<p class="text-sm text-muted-foreground">→ {fmt.format(value.toDate('UTC'))}</p>
{/if}
</div>DatePicker props
DateRangePicker shares the same shape; its `value` is a start/end object.
| Prop | Type | Default | Description |
|---|---|---|---|
DateValue | undefined (bindable) | undefined | Currently selected date. Two-way bindable. | |
boolean (bindable) | false | Whether the popover is open. | |
string | 'Pick a date' | Trigger label when no value is selected. | |
boolean | false | Disables the trigger button. | |
string | 'en-US' | BCP 47 locale used to format the selected date via Intl.DateFormatter. | |
string | — | Passed to the trigger Button for width or font overrides. | |
string | — | Passed to the Popover.Content wrapping the calendar. |