8.0k New

Date Picker

A date picker component with range and presets.

Epicenter

Local-first, open source apps

Special Sponsor
<script lang="ts">
  import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
  import {
    DateFormatter,
    type DateValue,
    getLocalTimeZone
  } from "@internationalized/date";
  import * as Popover from "$lib/components/ui/popover/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
 
  const df = new DateFormatter("en-US", {
    dateStyle: "long"
  });
 
  let value = $state<DateValue | undefined>();
</script>
 
<Popover.Root>
  <Popover.Trigger>
    {#snippet child({ props })}
      <Button
        {...props}
        variant="outline"
        data-empty={!value}
        class="w-[212px] justify-between text-start font-normal data-[empty=true]:text-muted-foreground"
      >
        {value ? df.format(value.toDate(getLocalTimeZone())) : "Pick a date"}
        <ChevronDownIcon data-icon="inline-end" />
      </Button>
    {/snippet}
  </Popover.Trigger>
  <Popover.Content class="w-auto p-0" align="start">
    <Calendar type="single" bind:value />
  </Popover.Content>
</Popover.Root>

Installation

The Date Picker is built using a composition of the <Popover /> and either the <Calendar /> or <RangeCalendar /> components.

See installations instructions for the Popover, Calendar, and Range Calendar components.

Usage

lib/components/example-date-picker.svelte
<script lang="ts">
  import CalendarIcon from "@lucide/svelte/icons/calendar";
  import {
    type DateValue,
    DateFormatter,
    getLocalTimeZone,
  } from "@internationalized/date";
  import { cn } from "$lib/utils.js";
  import { Button } from "$lib/components/ui/button/index.js";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
  import * as Popover from "$lib/components/ui/popover/index.js";
 
  const df = new DateFormatter("en-US", {
    dateStyle: "long",
  });
 
  let value = $state<DateValue>();
</script>
 
<Popover.Root>
  <Popover.Trigger>
    {#snippet child({ props })}
      <Button
        variant="outline"
        class={cn(
          "w-[280px] justify-start text-start font-normal",
          !value && "text-muted-foreground"
        )}
        {...props}
      >
        <CalendarIcon class="me-2 size-4" />
        {value ? df.format(value.toDate(getLocalTimeZone())) : "Select a date"}
      </Button>
    {/snippet}
  </Popover.Trigger>
  <Popover.Content class="w-auto p-0">
    <Calendar bind:value type="single" initialFocus captionLayout="dropdown" />
  </Popover.Content>
</Popover.Root>

Composition

A date picker is built from Popover and Calendar (there is no DatePicker root component):

Popover.Root
├── Popover.Trigger
└── Popover.Content
    └── Calendar

Basic

A basic date picker component.

<script lang="ts">
  import {
    DateFormatter,
    type DateValue,
    getLocalTimeZone
  } from "@internationalized/date";
  import * as Field from "$lib/components/ui/field/index.js";
  import * as Popover from "$lib/components/ui/popover/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
 
  const df = new DateFormatter("en-US", {
    dateStyle: "long"
  });
 
  let value = $state<DateValue | undefined>();
</script>
 
<Field.Field class="mx-auto w-44">
  <Field.Label for="date-picker-simple">Date</Field.Label>
  <Popover.Root>
    <Popover.Trigger id="date-picker-simple">
      {#snippet child({ props })}
        <Button {...props} variant="outline" class="justify-start font-normal">
          {value ? df.format(value.toDate(getLocalTimeZone())) : "Pick a date"}
        </Button>
      {/snippet}
    </Popover.Trigger>
    <Popover.Content class="w-auto p-0" align="start">
      <Calendar type="single" bind:value />
    </Popover.Content>
  </Popover.Root>
</Field.Field>

Range Picker

A date picker component for selecting a range of dates.

<script lang="ts">
  import CalendarIcon from "@lucide/svelte/icons/calendar";
  import {
    CalendarDate,
    DateFormatter,
    getLocalTimeZone,
    today
  } from "@internationalized/date";
  import * as Field from "$lib/components/ui/field/index.js";
  import * as Popover from "$lib/components/ui/popover/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
  import { RangeCalendar } from "$lib/components/ui/range-calendar/index.js";
  import type { DateRange } from "bits-ui";
 
  const df = new DateFormatter("en-US", {
    dateStyle: "medium"
  });
 
  const year = today(getLocalTimeZone()).year;
 
  let value = $state<DateRange | undefined>({
    start: new CalendarDate(year, 1, 20),
    end: new CalendarDate(year, 1, 20).add({ days: 20 })
  });
</script>
 
<Field.Field class="mx-auto w-60">
  <Field.Label for="date-picker-range">Date Picker Range</Field.Label>
  <Popover.Root>
    <Popover.Trigger id="date-picker-range">
      {#snippet child({ props })}
        <Button
          {...props}
          variant="outline"
          class="justify-start px-2.5 font-normal"
        >
          <CalendarIcon data-icon="inline-start" />
          {#if value?.start}
            {#if value.end}
              {df.format(value.start.toDate(getLocalTimeZone()))} - {df.format(
                value.end.toDate(getLocalTimeZone())
              )}
            {:else}
              {df.format(value.start.toDate(getLocalTimeZone()))}
            {/if}
          {:else}
            <span>Pick a date</span>
          {/if}
        </Button>
      {/snippet}
    </Popover.Trigger>
    <Popover.Content class="w-auto p-0" align="start">
      <RangeCalendar bind:value numberOfMonths={2} />
    </Popover.Content>
  </Popover.Root>
</Field.Field>

Date of Birth

A date picker component for selecting a date of birth. This component includes a dropdown caption layout for date and month selection.

<script lang="ts">
  import { getLocalTimeZone, type DateValue } from "@internationalized/date";
  import * as Field from "$lib/components/ui/field/index.js";
  import * as Popover from "$lib/components/ui/popover/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
 
  let open = $state(false);
  let value = $state<DateValue | undefined>();
</script>
 
<Field.Field class="mx-auto w-44">
  <Field.Label for="date">Date of birth</Field.Label>
  <Popover.Root bind:open>
    <Popover.Trigger id="date">
      {#snippet child({ props })}
        <Button {...props} variant="outline" class="justify-start font-normal">
          {value
            ? value.toDate(getLocalTimeZone()).toLocaleDateString()
            : "Select date"}
        </Button>
      {/snippet}
    </Popover.Trigger>
    <Popover.Content class="w-auto overflow-hidden p-0" align="start">
      <Calendar
        type="single"
        bind:value
        captionLayout="dropdown"
        onValueChange={() => {
          open = false;
        }}
      />
    </Popover.Content>
  </Popover.Root>
</Field.Field>

Input

A date picker component with an input field for selecting a date.

<script lang="ts">
  import CalendarIcon from "@lucide/svelte/icons/calendar";
  import {
    CalendarDate,
    getLocalTimeZone,
    type DateValue
  } from "@internationalized/date";
  import { untrack } from "svelte";
  import * as Field from "$lib/components/ui/field/index.js";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
  import * as Popover from "$lib/components/ui/popover/index.js";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
 
  function formatDate(date: DateValue | undefined) {
    if (!date) return "";
 
    return date.toDate(getLocalTimeZone()).toLocaleDateString("en-US", {
      day: "2-digit",
      month: "long",
      year: "numeric"
    });
  }
 
  function isValidDate(date: Date | undefined): date is Date {
    if (!date) return false;
    return !isNaN(date.getTime());
  }
 
  let open = $state(false);
  let value = $state<DateValue | undefined>(new CalendarDate(2025, 6, 1));
  let month = $state<DateValue | undefined>(untrack(() => value));
  let inputValue = $state(untrack(() => formatDate(value)));
</script>
 
<Field.Field class="mx-auto w-48">
  <Field.Label for="date-required">Subscription Date</Field.Label>
  <InputGroup.Root>
    <InputGroup.Input
      id="date-required"
      placeholder="June 01, 2025"
      bind:value={
        () => inputValue,
        (v) => {
          inputValue = v;
          const date = new Date(v);
          if (isValidDate(date)) {
            value = new CalendarDate(
              date.getFullYear(),
              date.getMonth() + 1,
              date.getDate()
            );
            month = value;
          }
        }
      }
      onkeydown={(e) => {
        if (e.key === "ArrowDown") {
          e.preventDefault();
          open = true;
        }
      }}
    />
    <InputGroup.Addon align="inline-end">
      <Popover.Root bind:open>
        <Popover.Trigger id="date-picker">
          {#snippet child({ props })}
            <InputGroup.Button
              {...props}
              variant="ghost"
              size="icon-xs"
              aria-label="Select date"
            >
              <CalendarIcon />
              <span class="sr-only">Select date</span>
            </InputGroup.Button>
          {/snippet}
        </Popover.Trigger>
        <Popover.Content
          class="w-auto overflow-hidden p-0"
          align="end"
          alignOffset={-8}
          sideOffset={10}
        >
          <Calendar
            type="single"
            bind:value
            bind:placeholder={month}
            onValueChange={(v) => {
              inputValue = formatDate(v);
              open = false;
            }}
          />
        </Popover.Content>
      </Popover.Root>
    </InputGroup.Addon>
  </InputGroup.Root>
</Field.Field>

Time Picker

A date picker component with a time input field for selecting a time.

<script lang="ts">
  import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
  import {
    DateFormatter,
    getLocalTimeZone,
    type DateValue
  } from "@internationalized/date";
  import * as Field from "$lib/components/ui/field/index.js";
  import * as Popover from "$lib/components/ui/popover/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
  import { Input } from "$lib/components/ui/input/index.js";
 
  const df = new DateFormatter("en-US", {
    dateStyle: "long"
  });
 
  let open = $state(false);
  let value = $state<DateValue | undefined>();
</script>
 
<Field.Group class="mx-auto max-w-xs flex-row">
  <Field.Field>
    <Field.Label for="date-picker-optional">Date</Field.Label>
    <Popover.Root bind:open>
      <Popover.Trigger id="date-picker-optional">
        {#snippet child({ props })}
          <Button
            {...props}
            variant="outline"
            class="w-32 justify-between font-normal"
          >
            {value
              ? df.format(value.toDate(getLocalTimeZone()))
              : "Select date"}
            <ChevronDownIcon data-icon="inline-end" />
          </Button>
        {/snippet}
      </Popover.Trigger>
      <Popover.Content class="w-auto overflow-hidden p-0" align="start">
        <Calendar
          type="single"
          bind:value
          captionLayout="dropdown"
          onValueChange={() => {
            open = false;
          }}
        />
      </Popover.Content>
    </Popover.Root>
  </Field.Field>
  <Field.Field class="w-32">
    <Field.Label for="time-picker-optional">Time</Field.Label>
    <Input
      type="time"
      id="time-picker-optional"
      step="1"
      value="10:30:00"
      class="appearance-none bg-background [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
    />
  </Field.Field>
</Field.Group>

Natural Language Picker

This component uses the chrono-node library to parse natural language dates.

Your post will be published on October 01, 2026.
<script lang="ts">
  import CalendarIcon from "@lucide/svelte/icons/calendar";
  import {
    CalendarDate,
    getLocalTimeZone,
    type DateValue
  } from "@internationalized/date";
  import { parseDate } from "chrono-node";
  import { untrack } from "svelte";
  import * as Field from "$lib/components/ui/field/index.js";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
  import * as Popover from "$lib/components/ui/popover/index.js";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
 
  function formatDate(date: DateValue | undefined) {
    if (!date) return "";
 
    return date.toDate(getLocalTimeZone()).toLocaleDateString("en-US", {
      day: "2-digit",
      month: "long",
      year: "numeric"
    });
  }
 
  function toCalendarDate(date: Date) {
    return new CalendarDate(
      date.getFullYear(),
      date.getMonth() + 1,
      date.getDate()
    );
  }
 
  let open = $state(false);
  let inputValue = $state("In 2 days");
  let value = $state<DateValue | undefined>(
    untrack(() => {
      const date = parseDate(inputValue);
      return date ? toCalendarDate(date) : undefined;
    })
  );
</script>
 
<Field.Field class="mx-auto max-w-xs">
  <Field.Label for="date-optional">Schedule Date</Field.Label>
  <InputGroup.Root>
    <InputGroup.Input
      id="date-optional"
      placeholder="Tomorrow or next week"
      bind:value={
        () => inputValue,
        (v) => {
          inputValue = v;
          const date = parseDate(v);
          if (date) {
            value = toCalendarDate(date);
          }
        }
      }
      onkeydown={(e) => {
        if (e.key === "ArrowDown") {
          e.preventDefault();
          open = true;
        }
      }}
    />
    <InputGroup.Addon align="inline-end">
      <Popover.Root bind:open>
        <Popover.Trigger id="date-picker">
          {#snippet child({ props })}
            <InputGroup.Button
              {...props}
              variant="ghost"
              size="icon-xs"
              aria-label="Select date"
            >
              <CalendarIcon />
              <span class="sr-only">Select date</span>
            </InputGroup.Button>
          {/snippet}
        </Popover.Trigger>
        <Popover.Content
          class="w-auto overflow-hidden p-0"
          align="end"
          sideOffset={8}
        >
          <Calendar
            type="single"
            bind:value
            captionLayout="dropdown"
            onValueChange={(v) => {
              inputValue = formatDate(v);
              open = false;
            }}
          />
        </Popover.Content>
      </Popover.Root>
    </InputGroup.Addon>
  </InputGroup.Root>
  <div class="px-1 text-sm text-muted-foreground">
    Your post will be published on
    <span class="font-medium">{formatDate(value)}</span>.
  </div>
</Field.Field>