Skip to main content
Urbicon UI

Calendar

Feature-rich calendar with month, week, day, year, and agenda views. Supports events, time grid, drag & drop, recurrence, date range selection, and custom rendering.

Playground

March 2026
Today
MonTueWedThuFriSatSun
Meeting
Deadline
Fokuszeit
Social
Variant
Size
Selection Mode
<script lang="ts">
  import { Calendar } from '@urbicon-ui/blocks';

  const events = [
    {
      id: '1',
      title: 'Sprint Planning',
      start: new Date('2026-03-02T00:00:00.000Z'),
      categoryId: 'meeting',
      description: 'Sprint 14 Planung'
    },
    {
      id: '2',
      title: 'Design Review',
      start: new Date('2026-03-05T00:00:00.000Z'),
      categoryId: 'meeting'
    },
    {
      id: '3',
      title: 'Release v3.0',
      start: new Date('2026-03-07T00:00:00.000Z'),
      categoryId: 'deadline'
    },
    {
      id: '4',
      title: 'Deep Work',
      start: new Date('2026-03-09T10:00:00.000Z'),
      end: new Date('2026-03-09T12:00:00.000Z'),
      allDay: false,
      categoryId: 'focus'
    },
    {
      id: '5',
      title: 'Team Lunch',
      start: new Date('2026-03-10T00:00:00.000Z'),
      categoryId: 'social'
    },
    {
      id: '6',
      title: 'Standup',
      start: new Date('2026-03-10T09:00:00.000Z'),
      end: new Date('2026-03-10T09:30:00.000Z'),
      allDay: false,
      categoryId: 'meeting'
    },
    {
      id: '7',
      title: '1:1 Sarah',
      start: new Date('2026-03-11T14:00:00.000Z'),
      end: new Date('2026-03-11T15:00:00.000Z'),
      allDay: false,
      categoryId: 'meeting'
    },
    { id: '8', title: 'Retro', start: new Date('2026-03-12T00:00:00.000Z'), categoryId: 'meeting' },
    {
      id: '9',
      title: 'Code Freeze',
      start: new Date('2026-03-14T00:00:00.000Z'),
      categoryId: 'deadline'
    },
    {
      id: '10',
      title: 'Konferenz',
      start: new Date('2026-03-16T00:00:00.000Z'),
      end: new Date('2026-03-18T00:00:00.000Z'),
      categoryId: 'social',
      description: 'SvelteConf Berlin'
    },
    {
      id: '11',
      title: 'Sprint Review',
      start: new Date('2026-03-19T00:00:00.000Z'),
      categoryId: 'meeting'
    },
    {
      id: '12',
      title: 'Hackathon',
      start: new Date('2026-03-23T00:00:00.000Z'),
      end: new Date('2026-03-24T00:00:00.000Z'),
      categoryId: 'social'
    },
    {
      id: '13',
      title: 'Quartalsbericht',
      start: new Date('2026-03-25T00:00:00.000Z'),
      categoryId: 'deadline'
    },
    {
      id: '14',
      title: 'Sprint Planning',
      start: new Date('2026-03-26T00:00:00.000Z'),
      categoryId: 'meeting'
    },
    {
      id: '15',
      title: 'Go Live',
      start: new Date('2026-03-31T00:00:00.000Z'),
      categoryId: 'deadline'
    }
  ];
  const categories = [
    { id: 'meeting', label: 'Meeting', color: '#8b5cf6' },
    { id: 'deadline', label: 'Deadline', color: '#ef4444' },
    { id: 'focus', label: 'Fokuszeit', color: '#3b82f6' },
    { id: 'social', label: 'Social', color: '#06b6d4' }
  ];
</script>

<Calendar
  {events}
  {categories}
/>

01 Examples

Events in a month view

The month grid is the default and carries the whole event model. A one-day event needs only start; adding end draws it across a span (conferences, sprints, holidays); adding a recurrence rule expands one object into a series (a weekday standup, a biweekly review). Categories colour the entries and drive the legend, and clicking a day opens its detail list.
März 2026
Today
MoDiMiDoFrSaSo
9
10
11
12
13
14
Meeting
Deadline
Social
Gesundheit
März 2026
Today
MoDiMiDoFrSaSo
SvelteConf
Sprint 14
Skiurlaub
Hackathon
Release Week
Konferenz
Urlaub
Sprint
Deadline
March 2026
Today
MonTueWedThuFriSatSun
9
10
11
12
13
14
Standup
Review
Sport
Waste collection
import type { CalendarEvent } from '@urbicon-ui/blocks';

const events: CalendarEvent[] = [
  // One day: start only.
  { id: '1', title: 'Code freeze', start: new Date(2026, 2, 18), categoryId: 'deadline' },

  // A span: add end, and the event draws across every day between.
  { id: '2', title: 'Sprint 14', start: new Date(2026, 2, 9), end: new Date(2026, 2, 20) },

  // A series: one object plus a rule, expanded by the calendar.
  // byDay is 0-6 (Sunday-Saturday): on `weekly` it generates one occurrence
  // per listed day, on `daily` it filters the days the interval produces.
  // interval skips n periods; until ends the series (inclusive).
  {
    id: '3',
    title: 'Standup',
    start: new Date(2026, 2, 2),
    recurrence: {
      frequency: 'weekly',
      byDay: [1, 2, 3, 4, 5],
      until: new Date(2026, 2, 31)
    }
  },
  {
    id: '4',
    title: 'Sprint review',
    start: new Date(2026, 2, 6),
    recurrence: { frequency: 'weekly', interval: 2, byDay: [5] }
  }
];

<Calendar {events} {categories} showLegend showWeekNumbers />

Week, year and agenda views

Same component, same events, one prop. Pick week with a time grid when the hour matters, year when the question is distribution across months, and agenda when the reader wants a list rather than a grid. The snippet shows only what differs between the three.
KW 11 – März 2026
Today
08:00
08:30
09:00
09:30
10:00
10:30
11:00
11:30
12:00
12:30
13:00
13:30
14:00
14:30
15:00
15:30
16:00
16:30
17:00
17:30
Meeting
Fokuszeit
Pause
2026
Today
Release
Feiertag
Urlaub
March 2026
Today
Tuesday, March 10, 2026
Sprint Planning
Sprint 14 planning with the team
Wednesday, March 11, 2026
Dentist
10:00 AM
Code Review
Thursday, March 12, 2026
Yoga class
6:00 PM
Saturday, March 14, 2026
Release v3.0
Production Deployment
Sunday, March 15, 2026
Max's birthday party
Monday, March 16, 2026
Retro
Wednesday, March 18, 2026
Parent-teacher night
7:30 PM
Friday, March 20, 2026
Hackathon
Build an AI prototype
Saturday, March 21, 2026
Hackathon
Demo & presentation
Work
Personal
Health
<!-- One component, one prop. The default view is "month". -->
<Calendar
  view="week"
  {events}
  {categories}
  showTimeGrid
  timeGridStartHour={8}
  timeGridEndHour={18}
  timeGridInterval={30}
/>

<Calendar view="year" views={['month', 'year']} {events} {categories} defaultYear={2026} />

<Calendar view="agenda" {events} {categories} agendaDays={21} />

Constrained selection

Two ways to narrow what a reader may pick, usually used together: selectionMode='range' takes a start and an end in two clicks, while minDate/maxDate bound the navigable window, disabledDates locks named days such as public holidays, and isDateDisabled locks a rule such as weekends.
March 2026
Today
MonTueWedThuFriSatSun
March 2026
Today
MonTueWedThuFriSatSun
<!-- Two clicks pick a range; minDate/maxDate bound both ends. -->
<Calendar
  selectionMode="range"
  bind:value
  variant="bordered"
  minDate={new Date(2026, 2, 1)}
  maxDate={new Date(2026, 3, 30)}
/>

<!-- disabledDates locks named days, isDateDisabled locks a rule. -->
<Calendar
  bind:value={selectedDate}
  variant="bordered"
  {minDate}
  {maxDate}
  disabledDates={holidays}
  isDateDisabled={(d) => d.getDay() === 0 || d.getDay() === 6}
/>

Custom day cells – heatmap

Full control over day-cell rendering with the dayCell snippet — the escape hatch for anything the event model does not express. Here as a GitHub-style activity heatmap: the more events on a day, the more intense the green.
März 2026
Today
MoDiMiDoFrSaSo
Weniger Mehr
<!-- urbicon-ignore raw-tailwind-color — the emerald ramp IS the demo: a
     contribution-heatmap day cell, where four fixed opacity steps of one hue encode
     the count. A semantic token has one value and cannot express a scale. -->
<script lang="ts">
  import { Calendar } from '@urbicon-ui/blocks';
  import type { CalendarEvent, DayCellContext } from '@urbicon-ui/blocks';

  // Realistic activity data spread across the month
  const events: CalendarEvent[] = [
    { id: '1', title: 'Standup', start: new Date(2026, 2, 2) },
    { id: '2', title: 'Code Review', start: new Date(2026, 2, 3) },
    { id: '3', title: 'Standup', start: new Date(2026, 2, 3) },
    { id: '4', title: 'Standup', start: new Date(2026, 2, 4) },
    { id: '5', title: 'Planning', start: new Date(2026, 2, 5) },
    { id: '6', title: 'Standup', start: new Date(2026, 2, 5) },
    { id: '7', title: 'Review', start: new Date(2026, 2, 5) },
    { id: '8', title: 'Standup', start: new Date(2026, 2, 9) },
    { id: '9', title: 'Deep Work', start: new Date(2026, 2, 9) },
    { id: '10', title: 'Standup', start: new Date(2026, 2, 10) },
    { id: '11', title: 'Release', start: new Date(2026, 2, 10) },
    { id: '12', title: 'Hotfix', start: new Date(2026, 2, 10) },
    { id: '13', title: 'Monitoring', start: new Date(2026, 2, 10) },
    { id: '14', title: 'Standup', start: new Date(2026, 2, 11) },
    { id: '15', title: 'Standup', start: new Date(2026, 2, 12) },
    { id: '16', title: 'Retro', start: new Date(2026, 2, 12) },
    { id: '17', title: 'Standup', start: new Date(2026, 2, 16) },
    { id: '18', title: 'Sprint Review', start: new Date(2026, 2, 16) },
    { id: '19', title: 'Demo', start: new Date(2026, 2, 16) },
    { id: '20', title: 'Standup', start: new Date(2026, 2, 17) },
    { id: '21', title: 'Hackathon', start: new Date(2026, 2, 19) },
    { id: '22', title: 'Hackathon', start: new Date(2026, 2, 19) },
    { id: '23', title: 'Hackathon', start: new Date(2026, 2, 19) },
    { id: '24', title: 'Demo', start: new Date(2026, 2, 19) },
    { id: '25', title: 'Hackathon', start: new Date(2026, 2, 19) },
    { id: '26', title: 'Standup', start: new Date(2026, 2, 20) },
    { id: '27', title: 'Standup', start: new Date(2026, 2, 23) },
    { id: '28', title: 'Planning', start: new Date(2026, 2, 23) },
    { id: '29', title: 'Standup', start: new Date(2026, 2, 24) },
    { id: '30', title: 'Review', start: new Date(2026, 2, 25) },
    { id: '31', title: 'Standup', start: new Date(2026, 2, 25) },
    { id: '32', title: 'Deploy', start: new Date(2026, 2, 25) },
    { id: '33', title: 'Standup', start: new Date(2026, 2, 26) },
    { id: '34', title: 'Standup', start: new Date(2026, 2, 30) },
    { id: '35', title: 'Quartalsbericht', start: new Date(2026, 2, 31) },
    { id: '36', title: 'Deploy', start: new Date(2026, 2, 31) },
    { id: '37', title: 'Review', start: new Date(2026, 2, 31) }
  ];

  /** Map event count to a heatmap intensity level (0–4) */
  function heatLevel(count: number): number {
    if (count === 0) return 0;
    if (count === 1) return 1;
    if (count === 2) return 2;
    if (count <= 4) return 3;
    return 4;
  }

  const heatBg = [
    '', // 0 – no bg
    'bg-emerald-500/15',
    'bg-emerald-500/30',
    'bg-emerald-500/50',
    'bg-emerald-500/75'
  ];

  // Heatmap ink, mode-aware via the CSS light-dark() function (darker emerald in
  // light mode, lighter in dark). This follows `color-scheme` natively — incl.
  // system mode, where there is no `.dark` class — so it needs no `dark:` override
  // (which the design linter flags and which would silently break in system mode).
  const heatText = [
    'text-text-primary', // 0 – no heat
    'text-[color:light-dark(var(--color-emerald-700),var(--color-emerald-300))]',
    'text-[color:light-dark(var(--color-emerald-800),var(--color-emerald-200))]',
    'text-[color:light-dark(var(--color-emerald-900),var(--color-emerald-100))]',
    'text-[color:light-dark(white,var(--color-emerald-950))]'
  ];
</script>

<div class="max-w-sm">
  <Calendar
    {events}
    showEventList
    showViewSwitcher={false}
    defaultMonth={2}
    defaultYear={2026}
    locale="de-DE"
  >
    {#snippet dayCell(ctx: DayCellContext)}
      {@const level = heatLevel(ctx.events.length)}
      <button
        class="flex h-10 w-full items-center justify-center rounded-md text-sm tabular-nums transition-all
          {ctx.isOutsideMonth ? 'opacity-20' : ''}
          {ctx.isDisabled ? 'cursor-not-allowed opacity-30' : 'cursor-pointer'}
          {ctx.isSelected ? 'ring-text-primary ring-2 ring-offset-1' : ''}
          {!ctx.isDisabled && !ctx.isSelected ? 'hover:ring-border-default hover:ring-1' : ''}
          {heatBg[level]}
          {ctx.isToday ? 'font-black underline decoration-2 underline-offset-2' : ''}
          {level > 0
          ? heatText[level]
          : ctx.isOutsideMonth
            ? 'text-text-quaternary'
            : 'text-text-primary'}"
      >
        {ctx.date.getDate()}
      </button>
    {/snippet}
  </Calendar>

  <!-- Heatmap legend -->
  <div class="mt-3 flex items-center justify-end gap-1.5 px-3">
    <span class="text-text-tertiary text-xs">Weniger</span>
    {#each [0, 1, 2, 3, 4] as lvl (lvl)}
      <span class="border-border-subtle size-3 rounded-sm border {heatBg[lvl] || 'bg-surface-base'}"
      ></span>
    {/each}
    <span class="text-text-tertiary text-xs">Mehr</span>
  </div>
</div>

02 Accessibility

ARIA Roles

The month grid uses role="grid" with role="row" for weeks and role="gridcell" for days. Each cell carries aria-selected, aria-disabled, and aria-current="date" for today.

Keyboard Navigation

move focus between days, between weeks. Home/End jump to the start/end of the week. PageUp/PageDown navigate between months. Enter/Space select the focused day. Focus rings use focus-visible: for keyboard-only visibility.

Screen Reader Labels

Every day cell has an aria-label with the full date (e.g. "Thursday, March 12, 2026"). Navigation buttons have descriptive labels. Event dots are aria-hidden; event details remain accessible through the event list.

Touch & Gestures

Horizontal swiping navigates between months/weeks/days. Touch input is handled through the Pointer Events API. Swipe gestures can be disabled via swipeable={false}. Animations respect prefers-reduced-motion.

Internationalization

All visible text and ARIA labels use i18n keys via bt(). Date formatting relies on the native Intl.DateTimeFormat with the configured locale. Weekday names, month names, and date formats adapt automatically.

03 API Reference

60 props
60 props
Prop
Type
Default
Description

04 Types

Local type definitions used by this component.

14 types
Name
Kind
Category
Used by
Description

05 Installation

Import

import { Calendar, CalendarHeader, CalendarGrid } from '@urbicon-ui/blocks';
import type { CalendarEvent, CalendarEventCategory } from '@urbicon-ui/blocks';