svelte-calendar Svelte Themes

Svelte Calendar

A themeable, pluggable **Svelte 5** calendar component library with **Day** and **Week** views ready for yoga studios, tour bookings, concerts, language schools, and more.

@nomideusz/svelte-calendar

A themeable Svelte 5 calendar with Day/Week Planner & Agenda views, touch-first mobile views, and a smart auto-theme that adapts to any page.

Installation

pnpm add @nomideusz/svelte-calendar

Requires Svelte 5 (^5.0.0)

Quick Start

<script>
  import { Calendar, createMemoryAdapter } from '@nomideusz/svelte-calendar';

  const adapter = createMemoryAdapter([
    { id: '1', title: 'Team Sync', start: new Date('2026-03-01T09:00'), end: new Date('2026-03-01T10:00') },
    { id: '2', title: 'Lunch',     start: new Date('2026-03-01T12:00'), end: new Date('2026-03-01T13:00') },
  ]);
</script>

<Calendar {adapter} />

That's it — 6 views (Day/Week × Planner, Agenda, Mobile), auto-coloring, drag-and-drop, all out of the box. The default auto theme probes your page's background, accent color, and fonts, so the calendar adapts to any design.

Views

Switch between Planner (time grid) and Agenda (list) in Day or Week mode:

<Calendar {adapter} view="week-planner" />  <!-- default -->
<Calendar {adapter} view="day-planner" />
<Calendar {adapter} view="week-agenda" />
<Calendar {adapter} view="day-agenda" />

Users can also switch via the built-in Day/Week pills.

Hide the pills when your app controls the view externally:

<Calendar {adapter} view="day-planner" showModePills={false} />

Mobile

On narrow screens (< 768px), the calendar automatically remaps Planner views to touch-first Mobile views with swipe navigation, a centralized header with Day/Week pills, and a compact layout:

<!-- Auto-detect (default) — switches at 768px -->
<Calendar {adapter} />

<!-- Force mobile layout -->
<Calendar {adapter} mobile={true} />

<!-- Force desktop layout -->
<Calendar {adapter} mobile={false} />

Mobile views include:

  • MobileDay — vertical time grid with hour labels, swipe left/right to change days, all-day event chips at the top, tap-to-create
  • MobileWeek — vertical day list showing each day's events with relative labels (Today, Tomorrow, etc.)

Agenda views keep their list-based layout on mobile but hide desktop floating navigation in favor of the centralized header.

Callbacks

<Calendar
  {adapter}
  oneventclick={(event) => console.log('Clicked', event.title)}
  oneventcreate={(range) => console.log('New slot', range.start, range.end)}
  oneventmove={(event, start, end) => console.log('Moved', event.title, start, end)}
  onviewchange={(viewId) => console.log('View', viewId)}
/>

Set readOnly to disable drag, resize, and click-to-create:

<Calendar {adapter} readOnly />

Hide nav controls (prev/next/today) and treat all days equally (no past-day dimming):

<!-- Yoga studio: fixed weekly schedule, no browsing, all days equal -->
<Calendar
  {adapter}
  view="week-agenda"
  readOnly
  showNavigation={false}
  showDates={false}
  equalDays
/>

Hide weekends for a workweek view:

<!-- Office planner: Mon–Fri only -->
<Calendar {adapter} view="week-planner" hideDays={[6, 7]} />

Control which date the calendar shows from your app:

<script>
  let date = $state(new Date());
</script>

<Calendar
  {adapter}
  currentDate={date}
  ondatechange={(d) => date = d}
/>

Show a rolling 3-day view:

<Calendar {adapter} view="week-planner" days={3} />

Block lunch hours and enforce 30–120 min events:

<script>
  import type { BlockedSlot } from '@nomideusz/svelte-calendar';

  const blocked: BlockedSlot[] = [
    { start: 12, end: 13, label: 'Lunch' },          // every day 12–1 PM
    { day: 6, start: 0, end: 24, label: 'Saturday' }, // block all Saturday
  ];
</script>

<Calendar {adapter} blockedSlots={blocked} minDuration={30} maxDuration={120} />

Disable specific dates:

<Calendar
  {adapter}
  disabledDates={[new Date('2026-03-25'), new Date('2026-04-01')]}
/>

Custom day headers and hover previews:

<Calendar {adapter} oneventhover={(ev) => showTooltip(ev)}>
  {#snippet dayHeader({ date, isToday, dayName })}
    <span style:font-weight={isToday ? 'bold' : 'normal'}>{dayName}</span>
  {/snippet}
</Calendar>

Themes

Three built-in presets:

Preset Description
auto Default. Probes the host page at mount — background, accent color, fonts, light/dark mode — and generates matching --dt-* tokens automatically. Reactively watches for host theme changes.
neutral Explicit light theme. White bg, blue accent, inherits host fonts. Use when embedding standalone.
midnight Explicit dark theme. Charcoal bg, red accent.
<script>
  import { Calendar, neutral, midnight } from '@nomideusz/svelte-calendar';
</script>

<Calendar {adapter} />                     <!-- auto: adapts to host page -->
<Calendar {adapter} theme={neutral} />     <!-- explicit light mode -->
<Calendar {adapter} theme={midnight} />    <!-- explicit dark mode -->

Smart Auto Theme

The default auto preset probes the host page's design and generates a calendar that blends in — no configuration needed. It detects:

  • Background color — from CSS variables, inline styles, or computed styles
  • Light/dark mode — from background luminance
  • Accent/brand color — from CSS variables (--accent, --primary, --bs-primary, etc.), link colors, or button colors
  • Text color — validated for contrast against the background
  • Fonts — inherited via CSS cascade

It also watches for changes (e.g. dark mode toggle) and updates automatically.

Fine-tune auto-detection with the autoTheme prop:

<!-- Force dark mode even if the page background is light -->
<Calendar {adapter} autoTheme={{ mode: 'dark' }} />

<!-- Override the accent color (skip probing) -->
<Calendar {adapter} autoTheme={{ accent: '#e11d48' }} />

<!-- Override the font stack -->
<Calendar {adapter} autoTheme={{ font: '"Poppins", sans-serif' }} />

<!-- Combine overrides -->
<Calendar {adapter} autoTheme={{ mode: 'dark', accent: '#10b981' }} />

<!-- Disable auto-probing entirely (passive CSS inheritance only) -->
<Calendar {adapter} autoTheme={false} />

Manual CSS Variables

Override any design token by setting --dt-* custom properties on an ancestor:

<!-- Wrap in a div with your overrides -->
<div style="--dt-accent: #e11d48; --dt-bg: #1a1a2e; --dt-text: rgba(255,255,255,0.87);">
  <Calendar {adapter} />
</div>

Or set them at the page level:

:root {
  --dt-bg: #fafafa;
  --dt-accent: #2563eb;
  --dt-text: rgba(0, 0, 0, 0.87);
  --dt-border: rgba(0, 0, 0, 0.08);
}

The auto-probe reads these first — if you set --dt-bg or --accent on :root, the calendar picks them up.

Extending Presets

Build on a preset by appending overrides:

import { neutral } from '@nomideusz/svelte-calendar';

// neutral base + custom accent + rounded feel
const custom = `${neutral}; --dt-accent: #e11d48;`;
<Calendar {adapter} theme={custom} />
All design tokens
Token Purpose
--dt-stage-bg Background behind the calendar (page area)
--dt-bg Calendar card background
--dt-surface Elevated surface (alternating rows, headers)
--dt-border Default border
--dt-border-day Day-column dividers
--dt-text Primary text
--dt-text-2 Secondary text
--dt-text-3 Tertiary text
--dt-accent Accent color
--dt-accent-dim Accent at ~12% opacity
--dt-glow Accent glow / focus ring
--dt-today-bg Today column highlight
--dt-btn-text Button label color
--dt-scrollbar Scrollbar thumb
--dt-success Completed indicator
--dt-serif / --dt-sans / --dt-mono Font stacks

Events

TimelineEvent

Field Type Description
id string Unique identifier
title string Event title
start / end Date Time range
color string? Accent color (auto-assigned if omitted)
category string? Grouping key — events with the same category share a color
subtitle string? Secondary text below the title
tags string[]? Small accent-colored pills
allDay boolean? Render as an all-day event
data Record? Arbitrary payload for your app

Auto-Coloring

Omit color and events are auto-assigned a vivid palette color, grouped by category (or title):

const events = [
  { id: '1', title: 'Yoga',  category: 'wellness', start: ..., end: ... },
  { id: '2', title: 'Pilates', category: 'wellness', start: ..., end: ... },  // same color
  { id: '3', title: 'Standup', start: ..., end: ... },                        // different color
];

Generate a theme-harmonious palette from any accent color:

import { createMemoryAdapter, generatePalette } from '@nomideusz/svelte-calendar';

// Colors that harmonize with your theme's accent
const palette = generatePalette('#e11d48');
const adapter = createMemoryAdapter(events, { palette });

Multi-day & All-day

Events spanning multiple days or flagged allDay: true render in a dedicated strip above timed events:

const events = [
  { id: '1', title: 'Conference', start: new Date('2026-03-15'), end: new Date('2026-03-18'), allDay: true },
  { id: '2', title: 'Sprint',    start: new Date('2026-03-15T00:00'), end: new Date('2026-03-17T00:00') },
];

Custom Event Rendering

Use the event snippet to fully control how events look:

<Calendar {adapter}>
  {#snippet event(ev)}
    <div style="padding: 4px 8px;">
      <strong>{ev.title}</strong>
      {#if ev.subtitle}<small>{ev.subtitle}</small>{/if}
    </div>
  {/snippet}
</Calendar>

Recurring Schedules

For fixed repeating events (class timetables, office hours):

<script>
  import { Calendar, createRecurringAdapter } from '@nomideusz/svelte-calendar';

  const adapter = createRecurringAdapter([
    { id: '1', title: 'Yoga',    dayOfWeek: 1, startTime: '07:00', endTime: '08:30' },
    { id: '2', title: 'Standup', frequency: 'daily', startTime: '09:00', endTime: '09:15',
      startDate: '2026-03-01', until: '2026-03-31' },
    { id: '3', title: 'Review',  frequency: 'monthly', dayOfMonth: 15,
      startTime: '10:00', endTime: '11:00' },
  ]);
</script>

<Calendar {adapter} readOnly />
RecurringEvent fields
Field Type Default Description
id string required Unique identifier
title string required Event title
startTime string required Start time "HH:MM"
endTime string required End time "HH:MM"
frequency 'daily' | 'weekly' | 'monthly' 'weekly' Recurrence frequency
interval number 1 Repeat every N periods (e.g. 2 = biweekly)
dayOfWeek number | number[] ISO weekday 1=Mon…7=Sun. Required for weekly.
dayOfMonth number 1 Day of month (1–31). For monthly.
startDate string First occurrence "YYYY-MM-DD"
until string Last occurrence "YYYY-MM-DD"
count number Max occurrences from startDate
color string? Accent color (auto-assigned if omitted)

REST Adapter

Connect to any REST API:

import { Calendar, createRestAdapter } from '@nomideusz/svelte-calendar';

const adapter = createRestAdapter({
  baseUrl: 'https://api.example.com/v1',
  headers: { Authorization: 'Bearer TOKEN' },
  // Optional: map your API shape to TimelineEvent[]
  mapEvents: (data) => data.items.map(item => ({
    id: item.id,
    title: item.name,
    start: new Date(item.startAt),
    end: new Date(item.endAt),
  })),
});

The adapter calls GET /events?start=...&end=..., POST /events, PATCH /events/:id, and DELETE /events/:id.

Custom Adapter

Implement the CalendarAdapter interface to connect any data source:

import type { CalendarAdapter, DateRange, TimelineEvent } from '@nomideusz/svelte-calendar';

const adapter: CalendarAdapter = {
  fetchEvents: async (range: DateRange) => { /* return TimelineEvent[] */ },
  createEvent: async (event) => { /* return created TimelineEvent with id */ },
  updateEvent: async (id, patch) => { /* return updated TimelineEvent */ },
  deleteEvent: async (id) => { /* void */ },
};

Localization (i18n)

The locale prop controls date/time formatting (BCP 47):

<Calendar {adapter} locale="de-DE" />

Override UI labels for full translation:

import { setLabels } from '@nomideusz/svelte-calendar';

setLabels({
  today: 'Heute', day: 'Tag', week: 'Woche',
  noEvents: 'Keine Termine',
  nMore: (n) => `+${n} weitere`,
});

Call resetLabels() to restore English defaults.

All label keys
Key Default Description
today 'Today' Relative day label / nav button
yesterday 'Yesterday' Relative day label
tomorrow 'Tomorrow' Relative day label
day 'Day' Mode pill
week 'Week' Mode pill
planner 'Planner' View label
agenda 'Agenda' View label
now 'now' Live indicator badge
free 'free' Empty slot hint
allDay 'All day' All-day event label
done 'Done' Past section header
upNext 'Up next' Upcoming section header
until 'until' Time-until prefix
noEvents 'No events' Empty day
nothingScheduled 'Nothing scheduled' Empty state
allDoneForToday 'All done for today' Completed state
goToToday 'Go to today' Nav button aria
previousDay / nextDay 'Previous day' / 'Next day' Nav aria
previousWeek / nextWeek 'Previous week' / 'Next week' Nav aria
calendar 'Calendar' Root region aria
nMore(n) `+${n} more` Overflow count
nEvents(n) `${n} event(s)` Event count aria
nCompleted(n) `${n} completed` Completed count
dayNOfTotal(i, t) `day ${i} of ${t}` Multi-day segment
percentComplete(p) `${p}% complete` Progress aria

Timezone Support

Convert events between timezones using the built-in helpers:

import { toZonedTime, fromZonedTime, nowInZone } from '@nomideusz/svelte-calendar';

// Display a UTC date in a specific timezone
const local = toZonedTime(utcDate, 'America/New_York');

// Convert back to UTC before saving
const utc = fromZonedTime(localDate, 'America/New_York');

// Current time in a timezone
const now = nowInZone('Asia/Tokyo');

Embeddable Widget

Drop into any HTML page — no build tools needed:

<script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>

<day-calendar
  api="https://myschool.com/api/events"
  theme="neutral"
  height="600"
></day-calendar>

All Props

Full Calendar props reference
Prop Type Default Description
adapter CalendarAdapter required Data layer (memory, recurring, REST, or custom)
views CalendarView[] all 4 built-in Registered view components
view string first view Active view ID
theme string auto CSS theme string (--dt-* custom properties). auto probes the host page and generates matching tokens.
autoTheme AutoThemeOptions | false {} Fine-tune auto-detection: { mode, accent, font }. Set false to disable probing.
mobile 'auto' | boolean 'auto' Mobile mode. 'auto' detects via matchMedia(<768px). Remaps Planner→Mobile views.
height number 600 Total height in pixels
borderRadius number 12 Border radius in pixels. Set to 0 for no rounding.
locale string 'en-US' BCP 47 locale tag
dir 'ltr' | 'rtl' | 'auto' Text direction
mondayStart boolean true Start week on Monday
readOnly boolean false Disable drag, resize, and click-to-create
visibleHours [number, number] Crop grid to [startHour, endHour)
initialDate Date today Date to focus on at mount
snapInterval number 15 Drag snap in minutes
showModePills boolean true Show the Day/Week mode pills
showNavigation boolean true Show prev/next/today navigation
equalDays boolean false Treat all days equally (no past-day dimming/collapsing)
showDates boolean true Show date numbers in headers. false = day names only (Mon, Tue, …)
hideDays number[] ISO weekdays to hide (1=Mon … 7=Sun). E.g. [6, 7] hides weekends
currentDate Date Controlled focus date (drives which date the calendar shows)
days number 7 Number of days shown in week views (e.g. 3 for a rolling 3-day view)
blockedSlots BlockedSlot[] Time ranges that cannot be booked (hatched overlay in planner views)
disabledDates Date[] Dates that are fully disabled (dimmed, no interaction)
minDuration number Minimum event duration in minutes (enforced on create & resize)
maxDuration number Maximum event duration in minutes (enforced on create & resize)
dayHeader Snippet<[{ date, isToday, dayName }]> Custom day header snippet for planner/agenda views
oneventclick (event) => void Event clicked
oneventcreate (range) => void New time range selected
oneventmove (event, start, end) => void Event dragged to new time
onviewchange (viewId) => void Active view changed
oneventhover (event) => void Pointer enters an event (for tooltips, previews)
ondatechange (date) => void Focused date changed (navigation, scroll, etc.)
event Snippet<[TimelineEvent]> Custom event rendering
empty Snippet Empty state content

Development

pnpm install
pnpm dev             # SvelteKit dev server
pnpm check           # Type check
pnpm run package     # Build library
pnpm run build:widget # Build widget.js

License

MIT

Top categories

Loading Svelte Themes