Getting Started

Installation

Add the scheduler to a SvelteKit project that already runs sv5ui.

1

Install the package

The scheduler ships separately from sv5ui and needs sv5ui 2.7.0 or later beside it. svelte and tailwindcss are peer dependencies, so the two packages share one copy of each. @internationalized/date, tailwind-variants and tailwind-merge come with the package and are the same versions sv5ui already carries.

npm
npm install @sv5ui/scheduler sv5ui
2

Import both themes

Tailwind 4 skips node_modules when it scans for class names, so a package has to register its own compiled output. The scheduler theme file is one @source line that does exactly that. It has to sit in the stylesheet that imports Tailwind itself, which sv5ui/theme.css already does.

src/routes/layout.css
@import 'tailwindcss';
@import 'sv5ui/theme.css';
@import '@sv5ui/scheduler/theme.css';

@custom-variant dark (&:where(.dark, .dark *));
3

Give it a height

The scheduler fills whatever height its layout gives it and scrolls inside. The common case is the rest of the page: put it in a flex column under your header with min-h-0 flex-1. A fixed height prop, or a parent with a height, works for a card or a dashboard tile.

src/routes/+layout.svelte
<script lang="ts">
  import { ModeWatcher } from 'mode-watcher';
  import './layout.css';

  let { children } = $props();
</script>

<ModeWatcher />

<div class="flex h-dvh flex-col">
  <header class="h-14 shrink-0 border-b">Your app header</header>
  {@render children()}
</div>
4

Render it

Bind an array and you have a working calendar. Every change the user makes through the UI is written back into that array. Set timeZone so the page renders the same on the server as it does in the browser.

src/routes/+page.svelte
<script lang="ts">
  import { Scheduler, type EventInput } from '@sv5ui/scheduler';

  let events = $state<EventInput[]>([
    { id: 'standup', title: 'Standup', start: '2026-09-14T09:00', end: '2026-09-14T09:15' },
    { id: 'offsite', title: 'Team offsite', start: '2026-09-17', end: '2026-09-19', allDay: true }
  ]);
</script>

<Scheduler bind:events timeZone="Asia/Ho_Chi_Minh" class="min-h-0 flex-1" />

Requirements

PackageVersion
Svelte5.x
Tailwind CSS4.x
sv5ui2.7.0 or later
@internationalized/date3.11 or later

Entry Points

Three imports cover the package.

EntryWhat it holds
@sv5ui/schedulerScheduler, the five views, the sidebar parts, EventChip, dragSource, config and every type
@sv5ui/scheduler/localesThirteen label packs, one export each: ar, de, en, es, fr, it, ja, ko, nl, pt, ru, vi, zh
@sv5ui/scheduler/theme.cssOne @source line that lets Tailwind see the classes inside the package
import { Scheduler, dragSource, defineSchedulerConfig } from '@sv5ui/scheduler';
import type { EventInput, Mutation, SchedulerLabels } from '@sv5ui/scheduler';

// Label packs live behind their own entry, so a bundle only carries the ones it imports
import { vi, ja } from '@sv5ui/scheduler/locales';

Dates You Build Yourself

Every date the scheduler hands back is a ZonedDateTime from @internationalized/date, the same library sv5ui's date pickers use. Passing ISO strings in and reading values out needs nothing more than the scheduler itself. The moment your own code constructs or types one of those values, install the package directly, and keep its version range in step with the scheduler's so the bundle holds one copy.

// The scheduler hands you ZonedDateTime values and accepts them back.
// Passing ISO strings needs nothing extra:
const events = [{ id: 'a', title: 'Call', start: '2026-09-14T09:00', end: '2026-09-14T09:30' }];

// Building or typing one yourself imports the package that defines it:
import { now, parseZonedDateTime, type ZonedDateTime } from '@internationalized/date';

let date = $state<ZonedDateTime>(now('Asia/Ho_Chi_Minh'));
const nextWeek = parseZonedDateTime('2026-09-21T09:00[Asia/Ho_Chi_Minh]');

// Under pnpm a package that only arrives as a dependency of a dependency
// cannot be imported, so add it to your own package.json:
//   pnpm add @internationalized/date

App-wide Defaults

Set the slot classes every scheduler in the app starts from. A scheduler reads the config when it is created, so the call has to run before any scheduler renders. Keep it in src/scheduler.config.ts and import that file once from the root layout, the same way an app keeps sv5ui's own defaults in a config file of its own. Each call replaces the previous config rather than merging into it. The Theming page lists what can be set.

src/scheduler.config.ts
// src/scheduler.config.ts
import { defineSchedulerConfig } from '@sv5ui/scheduler';

defineSchedulerConfig({
  scheduler: { slots: { root: 'rounded-2xl border border-outline-variant/60' } },
  eventChip: { slots: { title: 'font-medium' } }
});
src/routes/+layout.svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import '../scheduler.config';
</script>

Rendering on the Server

The scheduler renders on the server without touching window. The one thing to decide is the time zone: its default is the zone of the machine doing the rendering, which is not the reader's when that machine is a server. See Time Zones for what the zone changes.

src/routes/+page.svelte
<!-- Without timeZone the scheduler uses the zone of the machine that renders:
     UTC on most servers, the reader's zone in the browser. The two renders
     then disagree about which day is today and where the current-time line
     sits, and hydration repaints the calendar. Pass the zone the user
     actually lives in, from their profile or a cookie. -->
<Scheduler bind:events timeZone={data.user.timeZone} />

Troubleshooting

The scheduler prints a warning once per cause in development instead of failing, so the console is the first place to look. These are the messages, and below them the setup problems that show up without one.

// Console messages the scheduler prints in development, and what they mean.

// An event whose end is not after its start, or whose dates cannot be parsed,
// is left out rather than breaking the calendar. Once per event id.
'[@sv5ui/scheduler] Event x was skipped: Event x must end after it starts'

// dayStartHour and dayEndHour must be whole hours inside one day, start before end.
'[@sv5ui/scheduler] Visible hours 8 to 8 are not a range inside one day; showing the whole day.'

// hiddenDays lists every day of the week.
'[@sv5ui/scheduler] hiddenDays hides every day of the week; showing all of them.'

// A recurrence rule uses fields the built-in engine does not evaluate.
'[@sv5ui/scheduler] Event "x" uses recurrence fields the built-in engine does not evaluate: byMonthDay.'

// Thrown, not printed: view names the scheduler does not know.
'Unknown view "workweek". Known: day, week, month, year, agenda'