Calendar
A date picker supporting single, multiple, and range selection. Built on @internationalized/date for timezone-aware dates.
Playground
Experiment with different props.
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
Basic Usage
Single date selection with bind:value.
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
<script lang="ts">
import { Calendar } from 'sv5ui';
import { today, getLocalTimeZone } from '@internationalized/date';
let value = $state(today(getLocalTimeZone()));
</script>
<Calendar bind:value />Variants
4 visual styles.
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
<Calendar variant="solid" />
<Calendar variant="outline" />
<Calendar variant="soft" />
<Calendar variant="subtle" />Sizes
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
<Calendar size="xs" />
<Calendar size="sm" />
<Calendar size="md" />
<Calendar size="lg" />
<Calendar size="xl" />Colors
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
<Calendar color="primary" />
<Calendar color="secondary" />
<Calendar color="tertiary" />
<Calendar color="success" />
<Calendar color="warning" />
<Calendar color="error" />
<Calendar color="info" />
<Calendar color="surface" />Multiple Selection
Select multiple dates with type="multiple".
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
Selected: 0 date(s)
<script lang="ts">
import { Calendar } from 'sv5ui';
import type { DateValue } from '@internationalized/date';
let values = $state<DateValue[]>([]);
</script>
<Calendar type="multiple" bind:value={values} />Multiple with maxDays
Cap how many dates the user can pick in type="multiple" mode with maxDays. Once the limit is reached, additional cells become unselectable.
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
Selected: 0 / 5 date(s)
<script lang="ts">
import { Calendar } from 'sv5ui';
import type { DateValue } from '@internationalized/date';
let values = $state<DateValue[]>([]);
</script>
<!-- Cap the selection to 5 dates -->
<Calendar type="multiple" maxDays={5} bind:value={values} />
<p>Selected: {values.length} / 5 date(s)</p>Highlighted Dates
Pass an isDateHighlightable(date) predicate to visually mark special dates such as holidays or events. Marked cells receive a data-marked attribute and render a small dot indicator. Selection and disabled state are not affected - override the dot via the cellTrigger slot or ui.cellTrigger using the data-[marked]: modifier.
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
<script lang="ts">
import { Calendar } from 'sv5ui';
import { today, getLocalTimeZone } from '@internationalized/date';
import type { DateValue } from '@internationalized/date';
let value = $state<DateValue | undefined>(undefined);
const now = today(getLocalTimeZone());
const holidays = [
now.add({ days: 3 }),
now.add({ days: 7 }),
now.add({ days: 10 }),
now.add({ days: 18 })
];
// Predicate runs for every visible cell - keep it cheap and pure.
const isHoliday = (date: DateValue) =>
holidays.some((h) => h.compare(date) === 0);
</script>
<Calendar bind:value isDateHighlightable={isHoliday} />
<!-- Marked cells get a data-marked attribute, which the default theme
renders as a small dot indicator. Override via the cellTrigger slot
or ui.cellTrigger using the data-[marked]: modifier. -->Multiple Months
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 26 | 27 | 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 | 1 | 2 | 3 | 4 | 5 |
<Calendar numberOfMonths={2} />Min & Max Date
Restrict selectable dates to a range.
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
<script lang="ts">
import { Calendar } from 'sv5ui';
import { today, getLocalTimeZone } from '@internationalized/date';
const now = today(getLocalTimeZone());
</script>
<Calendar
minValue={now}
maxValue={now.add({ days: 14 })}
/>Week Numbers
| # | Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|---|
| 1 | 28 | 29 | 30 | 1 | 2 | 3 | 4 |
| 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 3 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 4 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 5 | 26 | 27 | 28 | 29 | 30 | 31 | 1 |
| 6 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
<Calendar weekNumbers />Inside FormField
Wrap Calendar in <FormField> - when error is set, the calendar switches to the error color and aria-describedby wires up to the FormField's description and error text.
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 | 8 |
Please pick a date
<script lang="ts">
import { Calendar, FormField } from 'sv5ui';
import { today, getLocalTimeZone } from '@internationalized/date';
import type { DateValue } from '@internationalized/date';
let value = $state<DateValue | undefined>(undefined);
const now = today(getLocalTimeZone());
const error = $derived(value ? '' : 'Please pick a date');
</script>
<FormField
label="Appointment date"
hint="Required"
required
help="Pick any future date"
{error}
>
<Calendar bind:value minValue={now} />
</FormField>
<!-- When error is set, the Calendar:
- switches to the error color
- aria-describedby points to FormField's description + error text
- focus/blur/change events emit to the parent <Form> -->In a Popover
Combine with Popover for a date picker dropdown.
<script lang="ts">
import { Calendar, Popover, Button } from 'sv5ui';
import type { DateValue } from '@internationalized/date';
let value = $state<DateValue | undefined>(undefined);
let open = $state(false);
let display = $derived(value ? value.toString() : '');
</script>
<Popover bind:open>
<Button
variant="outline"
color="surface"
leadingIcon="lucide:calendar"
label={display || 'Pick a date'}
/>
{#snippet content()}
<div class="p-3">
<Calendar
bind:value
onValueChange={() => open = false}
size="sm"
/>
</div>
{/snippet}
</Popover>UI Slots
| Slot | Description |
|---|---|
root | Outermost wrapper |
header | Navigation and heading |
body | Main grid area |
navButton | Nav buttons |
navButtonIcon | Nav button icons |
grid | Calendar grid table |
heading | Month/year heading |
headCell | Weekday header cell |
cell | Date cell wrapper |
cellTrigger | Clickable date button |
Snippets
| Snippet | Description |
|---|---|
heading | Custom month/year heading (receives value string) |
day | Custom date cell content (receives day DateValue) |
weekDay | Custom weekday name (receives day string) |
Props
| Prop | Type | Default |
|---|---|---|
value | DateValue | DateValue[] | - |
type | 'single' | 'multiple' | 'single' |
range | boolean | false |
color | ColorType | 'primary' |
size | 'xs'|'sm'|'md'|'lg'|'xl' | 'md' |
variant | 'solid'|'outline'|'soft'|'subtle' | 'solid' |
numberOfMonths | number | 1 |
minValue | DateValue | - |
maxValue | DateValue | - |
isDateDisabled | DateMatcher | - |
isDateHighlightable | DateMatcher | - |
maxDays | number | - |
fixedWeeks | boolean | true |
weekNumbers | boolean | false |
weekStartsOn | 0-6 | 0 |
locale | string | 'en' |
monthControls | boolean | true |
yearControls | boolean | true |
disabled | boolean | false |
readonly | boolean | false |
id | string | - |
name | string | - |
ref | HTMLElement | null | null |
class | string | - |
ui | Record<Slot, Class> | - |