SvCalendar

A themeable, accessible month / year / decade calendar with every selection mode - the date surface the whole date/time family is built on.

Related: Date & time overview · SvDateTimePicker · SvDateRangeInput · SvTimePicker

Installation

Add it with the CLI - this drops a ready-to-edit SvCalendar starter into your app:

Prefer to see it first? npx @svgrid/ui try calendar opens it in a throwaway sandbox - no project needed.

Or install the package and import it directly. SvCalendar ships free in @svgrid/grid (no extra date library) and is the same component SvGrid mounts to edit a date cell - so it is both a standalone control and the grid's built-in date editor:

import { SvCalendar } from '@svgrid/grid'

Example

SvCalendar is a thin styled renderer over the headless createCalendar core, the same split as SvGrid: the core owns reactive state, selection, navigation, keyboard and ARIA, while the component keeps render-only concerns. Every color comes from the grid's --sg-* tokens, so it matches your grid and edit forms in light and dark. It is the editor SvGrid mounts for a date cell, and it works standalone anywhere.

Open the live example: Calendar (Date & Time)

<script lang="ts">
  import { SvCalendar } from '@svgrid/grid'
  let value = $state<Date[]>([new Date()])
</script>

<SvCalendar
  {value}
  selectionMode="range"
  firstDayOfWeek={1}
  weekNumbers
  footer
  onChange={(dates) => (value = dates)}
/>

Anatomy

The component is render-only; the reactive model lives in the headless createCalendar core. You almost never touch the core directly - SvCalendar wires it for you - but you can build a fully custom calendar on the same engine (see Headless editors):

<script lang="ts">
  import { SvCalendar } from '@svgrid/grid'
  let value = $state<Date[]>([])
</script>

<SvCalendar {value} onChange={(dates) => (value = dates)} />

Examples

Range selection with presets

Set selectionMode="range" and pass presets for one-click shortcuts. Function values resolve relative to today at click time.

Open the live example: Calendar - range + presets (Date & Time)

<SvCalendar
  selectionMode="range"
  months={2}
  presets={[
    { label: 'Last 7 days', value: () => [addDays(new Date(), -6), new Date()] },
    { label: 'This month', value: () => [startOfMonth(new Date()), new Date()] },
  ]}
  onChange={(dates) => (range = dates)}
/>

Date of birth

Single selection with max clamped to today so future dates are blocked, opening on the decade (year) grid for fewer clicks to a birth year.

Open the live example: Calendar - date of birth (Date & Time)

<SvCalendar
  {value}
  selectionMode="one"
  max={new Date()}
  displayMode="decade"
  footer
  onChange={(dates) => (value = dates)}
/>

Rich cells and recurrence (event calendars)

A day snippet fills each cell with event chips and switches to a taller grid; recurrence rules mark repeating days. The pure matchesRecurrence / expandRecurrence helpers generate the events.

A RecurrenceRule supports the patterns a real calendar needs - freq (daily / weekly / monthly / yearly), an interval, weekdays, a day of the month (negative counts from the end, -1 = last day), a positional weekOfMonth (1..4 or -1) paired with a single weekday ("the 2nd Tuesday", "the last Friday"), a yearly month, an anchor from, and an end condition - an inclusive until date or a count of occurrences:

{ freq: 'monthly', weekdays: [2], weekOfMonth: 1 }              // the first Tuesday
{ freq: 'monthly', day: -1 }                                   // the last day
{ freq: 'weekly',  weekdays: [1], count: 8, from: '2026-01-05' } // 8 times, then stop

Open the live example: Event calendar (rich cells + recurrence) (Date & Time)

<SvCalendar recurrence={rules}>
  {#snippet day(date, state)}
    <span class="num">{date.getDate()}</span>
    {#each eventsOn(date) as ev}
      <span class="chip">{ev.title}</span>
    {/each}
  {/snippet}
</SvCalendar>

Restricted and important dates

Block dates with restrictedDates (a list or predicate) and flag noteworthy ones with importantDates - restricted days are unselectable, important days stay selectable but carry an indicator:

<SvCalendar
  restrictedDates={(d) => d.getDay() === 0}
  importantDates={holidays}
  min={new Date()}
/>

Multi-day availability (many mode)

selectionMode="many" toggles any number of individual days - useful for picking shift days or blackout dates. Bind your own $state and assign the list back in onChange; min blocks past days:

<script lang="ts">
  import { SvCalendar, type CalendarValue } from '@svgrid/grid'
  let days = $state<Date[]>([])
</script>

<SvCalendar
  value={days as CalendarValue}
  selectionMode="many"
  min={new Date()}
  footer
  onChange={(picked) => (days = picked)}
/>
<p>{days.length} day(s) selected</p>

Tip: onChange always receives the complete selected-day array (not just the day that changed), in every selection mode - so you can assign it straight to state.

Props

Pass any of label / hint / error (plus required / invalid / id) to wrap the calendar in SvField chrome (a label + hint/error line) when using it as a standalone form control; omit them and it renders bare, as it does inside SvDateTimePicker's popover.

Prop Type Default Description
value CalendarValue null Selected value(s): a Date for single modes, an array for multi.
onChange (dates: Date[]) => void - Fires with the full selected-day list on every change.
onNavigate (viewDate: Date, displayMode: DisplayMode) => void - Fires when the visible month / year / decade page changes.
selectionMode one | zeroOrOne | many | zeroOrMany | oneOrMany | oneExtended | week | range one How selection behaves.
min / max DateLike | null null Selectable bounds.
restrictedDates list or predicate null Non-selectable dates.
importantDates ReadonlyArray<DateLike> | (d) => boolean | null null Highlighted (but still selectable) dates.
firstDayOfWeek number 0 0 = Sunday .. 6 = Saturday.
weeks number 6 Week rows per panel.
weekNumbers boolean false Show the ISO week-number column.
months number 1 Number of month panels side by side.
hideDayNames boolean false Hide the weekday header row.
hideOtherMonthDays boolean false Hide leading/trailing days (auto when months > 1).
dayNameFormat narrow | short | long short Weekday label length.
monthNameFormat narrow | short | long long Month/title label length.
footer boolean false Show the Today / Clear footer.
disabled boolean false Blocks interaction and dims the control.
readonly boolean false Shows the value but blocks changes.
locale string navigator BCP-47 locale for names and formatting.
name string - Emits a hidden input carrying the ISO dates.
displayMode DisplayMode month Which drill level to open on.
animate boolean | CalendarAnimation false Animate navigation / drill. true = slide (explicit opt-in).
wheelNavigation boolean false Change the visible page with the mouse wheel.
dateTooltip (date: Date) => string | null | undefined - Per-day native title text.
presets ReadonlyArray<CalendarPreset> - One-click shortcuts shown in a side rail.
recurrence RecurrenceRule | ReadonlyArray<RecurrenceRule> | null null Repeat pattern(s); matching days get a recurring state.
dir ltr | rtl | auto auto Text direction; rtl mirrors layout and flips nav arrows.
messages Partial<CalendarMessages> - Override the built-in strings.
day Snippet<[Date, CalendarDayState]> - Rich per-cell content (events, dots); switches to a taller grid.

Helper types

Accessibility

More examples

Calendar - headless

Styled SvCalendar and a compact custom day-grid built from panels / dayState / dayProps, sharing one value.

Open the live example: Calendar - headless (Headless Editors)

In a form

The shared field props behave the same on every editor: label names it, hint explains it, and error plus invalid mark it - which is why a validated form does not need per-component handling.

<script lang="ts">
  import { SvCalendar } from '@svgrid/grid'

  let calendar = $state<Date | null>(null)
</script>

<SvCalendar
  bind:value={calendar}
  label="Label"
  hint="A short hint"
  required
/>

<SvCalendar
  bind:value={calendar}
  label="Label"
  error="Something is wrong"
  invalid
/>

Disabled and read-only

Disabled takes the control out of the tab order; read-only keeps it focusable and copyable. Reach for read-only when the value still matters to the reader.

<script lang="ts">
  import { SvCalendar } from '@svgrid/grid'

  let calendar = $state<Date | null>(null)
</script>

<SvCalendar bind:value={calendar} disabled />

<SvCalendar bind:value={calendar} readonly />

See also

Live examples

  • Calendar - SvCalendar: a themeable month/year/decade calendar with single / range / week / multi selection, min-max, restricted + important dates and week numbers. The same component SvGrid mounts to edit a date cell - and usable standalone in any SvGrid app.
  • Calendar - range + presets - SvCalendar as a date-range picker: selectionMode="range" with a one-click presets rail (Today, Last 7 days, This month, Year to date...), animated month navigation and mouse-wheel scrolling. Presets resolve relative to today. Same component, no extra dependency.
  • Calendar - date of birth - SvCalendar as a date-of-birth picker: single selection with max clamped to today so future dates are blocked, opening on the decade (year) grid for fewer clicks to a birth year, with a live age readout. Same component SvGrid mounts to edit a date cell.
  • Event calendar (rich cells + recurrence) - SvCalendar extended into a FullCalendar-style scheduler: the `day` snippet fills each cell with event chips, and `recurrence` repeat patterns (weekly standups, every-other-week sprint, monthly invoices) mark repeating days and generate their events. Navigation, keyboard, selection and theming stay the component's. Pick a day for its agenda.
  • Calendar - headless - Styled SvCalendar and a compact custom day-grid built from panels / dayState / dayProps, sharing one value.