8.0k New

Calendar

A calendar component that allows users to select a date or a range of dates.

Docs API Reference

Epicenter

Local-first, open source apps

Special Sponsor
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script lang="ts">
  import {
    getLocalTimeZone,
    today,
    type CalendarDate
  } from "@internationalized/date";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
 
  let value = $state<CalendarDate | undefined>(today(getLocalTimeZone()));
</script>
 
<Calendar
  type="single"
  bind:value
  class="rounded-lg border"
  captionLayout="dropdown"
/>

Installation

pnpm dlx shadcn-svelte@latest add calendar

Usage

<script lang="ts">
  import { getLocalTimeZone, today } from "@internationalized/date";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
 
  let value = $state(today(getLocalTimeZone()));
</script>
<Calendar type="single" bind:value class="rounded-lg border" />

See the Bits UI Calendar documentation for more information.

About

The <Calendar /> component is built on top of the Bits UI Calendar component, which uses the @internationalized/date package to handle dates.

Date Picker

You can use the <Calendar /> component to build a date picker. See the Date Picker page for more information.

Basic

A basic calendar component. We used class="rounded-lg border" to style the calendar.

October 2026
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script lang="ts">
  import { Calendar } from "$lib/components/ui/calendar/index.js";
</script>
 
<Calendar type="single" class="rounded-lg border" />

Range Calendar

Use the RangeCalendar component to enable range selection.

January 2026
SuMoTuWeThFrSa
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
February 2026
SuMoTuWeThFrSa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script lang="ts">
  import {
    CalendarDate,
    getLocalTimeZone,
    today
  } from "@internationalized/date";
  import * as Card from "$lib/components/ui/card/index.js";
  import { RangeCalendar } from "$lib/components/ui/range-calendar/index.js";
  import type { DateRange } from "bits-ui";
 
  const start = new CalendarDate(today(getLocalTimeZone()).year, 1, 12);
 
  let value = $state<DateRange | undefined>({
    start,
    end: start.add({ days: 30 })
  });
</script>
 
<Card.Root class="mx-auto w-fit p-0">
  <Card.Content class="p-0">
    <RangeCalendar
      bind:value
      numberOfMonths={2}
      minValue={new CalendarDate(1900, 1, 1)}
      maxValue={today(getLocalTimeZone())}
    />
  </Card.Content>
</Card.Root>

Month and Year Selector

Use captionLayout="dropdown" to show month and year dropdowns.

SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script lang="ts">
  import { Calendar } from "$lib/components/ui/calendar/index.js";
</script>
 
<Calendar type="single" captionLayout="dropdown" class="rounded-lg border" />

Presets

October 2026
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
<script lang="ts">
  import {
    CalendarDate,
    getLocalTimeZone,
    today
  } from "@internationalized/date";
  import * as Card from "$lib/components/ui/card/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
 
  const presets = [
    { label: "Today", value: 0 },
    { label: "Tomorrow", value: 1 },
    { label: "In 3 days", value: 3 },
    { label: "In a week", value: 7 },
    { label: "In 2 weeks", value: 14 }
  ];
 
  let value = $state<CalendarDate | undefined>(
    new CalendarDate(today(getLocalTimeZone()).year, 2, 12)
  );
  let placeholder = $state<CalendarDate>(today(getLocalTimeZone()));
</script>
 
<Card.Root class="mx-auto w-fit max-w-[300px]" size="sm">
  <Card.Content>
    <Calendar
      type="single"
      bind:value
      bind:placeholder
      fixedWeeks
      class="p-0 [--cell-size:--spacing(9.5)]"
    />
  </Card.Content>
  <Card.Footer class="flex flex-wrap gap-2 border-t">
    {#each presets as preset (preset.value)}
      <Button
        variant="outline"
        size="sm"
        class="flex-1"
        onclick={() => {
          const newDate = today(getLocalTimeZone()).add({ days: preset.value });
          value = newDate;
          placeholder = newDate;
        }}
      >
        {preset.label}
      </Button>
    {/each}
  </Card.Footer>
</Card.Root>

Date and Time Picker

October 2026
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script lang="ts">
  import Clock2Icon from "@lucide/svelte/icons/clock-2";
  import {
    CalendarDate,
    getLocalTimeZone,
    today
  } from "@internationalized/date";
  import * as Card from "$lib/components/ui/card/index.js";
  import * as Field from "$lib/components/ui/field/index.js";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
 
  const now = today(getLocalTimeZone());
 
  let value = $state<CalendarDate | undefined>(
    new CalendarDate(now.year, now.month, 12)
  );
</script>
 
<Card.Root size="sm" class="mx-auto w-fit">
  <Card.Content>
    <Calendar type="single" bind:value class="p-0" />
  </Card.Content>
  <Card.Footer class="border-t bg-card">
    <Field.Group>
      <Field.Field>
        <Field.Label for="time-from">Start Time</Field.Label>
        <InputGroup.Root>
          <InputGroup.Input
            id="time-from"
            type="time"
            step="1"
            value="10:30:00"
            class="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
          />
          <InputGroup.Addon>
            <Clock2Icon class="text-muted-foreground" />
          </InputGroup.Addon>
        </InputGroup.Root>
      </Field.Field>
      <Field.Field>
        <Field.Label for="time-to">End Time</Field.Label>
        <InputGroup.Root>
          <InputGroup.Input
            id="time-to"
            type="time"
            step="1"
            value="12:30:00"
            class="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
          />
          <InputGroup.Addon>
            <Clock2Icon class="text-muted-foreground" />
          </InputGroup.Addon>
        </InputGroup.Root>
      </Field.Field>
    </Field.Group>
  </Card.Footer>
</Card.Root>

Booked dates

February 2026
SuMoTuWeThFrSa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script lang="ts">
  import {
    CalendarDate,
    getLocalTimeZone,
    today,
    type DateValue
  } from "@internationalized/date";
  import * as Card from "$lib/components/ui/card/index.js";
  import { Calendar } from "$lib/components/ui/calendar/index.js";
 
  const year = today(getLocalTimeZone()).year;
 
  let value = $state<CalendarDate | undefined>(new CalendarDate(year, 2, 3));
 
  const bookedDates = Array.from(
    { length: 15 },
    (_, i) => new CalendarDate(year, 2, 12 + i)
  );
 
  function isBooked(date: DateValue) {
    return bookedDates.some((d) => d.compare(date) === 0);
  }
</script>
 
<Card.Root class="mx-auto w-fit p-0">
  <Card.Content class="p-0">
    <Calendar type="single" bind:value isDateUnavailable={isBooked} />
  </Card.Content>
</Card.Root>

Custom Cell Size

SuMoTuWeThFrSa
29
30
1 $100
2 $100
3 $100
4 $100
5 $120
6 $120
7 $100
8 $100
9 $100
10 $100
11 $100
12 $120
13 $120
14 $100
15 $100
16 $100
17 $100
18 $100
19 $120
20 $120
21 $100
22 $100
23 $100
24 $100
25 $100
26 $120
27 $120
28 $100
29 $100
30 $100
31 $100
1
2
<script lang="ts">
  import {
    CalendarDate,
    getLocalTimeZone,
    isWeekend,
    today
  } from "@internationalized/date";
  import * as Card from "$lib/components/ui/card/index.js";
  import RangeCalendarDay from "$lib/components/ui/range-calendar/range-calendar-day.svelte";
  import { RangeCalendar } from "$lib/components/ui/range-calendar/index.js";
  import type { DateRange } from "bits-ui";
 
  const start = new CalendarDate(today(getLocalTimeZone()).year, 12, 8);
 
  let value = $state<DateRange | undefined>({
    start,
    end: start.add({ days: 10 })
  });
</script>
 
<Card.Root class="mx-auto w-fit p-0">
  <Card.Content class="p-0">
    <RangeCalendar
      bind:value
      captionLayout="dropdown"
      monthFormat="long"
      class="[--cell-size:--spacing(10)] md:[--cell-size:--spacing(12)]"
    >
      {#snippet day({ day, outsideMonth })}
        <RangeCalendarDay>
          {day.day}
          {#if !outsideMonth}
            <span>{isWeekend(day, "en-US") ? "$120" : "$100"}</span>
          {/if}
        </RangeCalendarDay>
      {/snippet}
    </RangeCalendar>
  </Card.Content>
</Card.Root>

You can customize the size of calendar cells using the --cell-size CSS variable. You can also make it responsive by using breakpoint-specific values:

<Calendar
  type="single"
  bind:value
  class="rounded-lg border [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/>

Or use fixed values:

<Calendar
  type="single"
  bind:value
  class="rounded-lg border [--cell-size:2.75rem] md:[--cell-size:3rem]"
/>