Quick Start
Five steps from an empty page to a calendar that saves. Each step adds a few lines to the one before it; the new lines are marked in the code.
1. Bind an array
Naive ISO strings are read in the time zone you give the scheduler, and end is exclusive. Drag a chip: the array under the calendar changes with it.
Oct 19 – 25, 2026
[
{
"id": "planning",
"start": "2026-10-19T10:00",
"end": "2026-10-19T11:30"
},
{
"id": "design",
"start": "2026-10-20T13:00",
"end": "2026-10-20T14:30"
},
{
"id": "gym",
"start": "2026-10-21T18:30",
"end": "2026-10-21T19:30"
},
{
"id": "offsite",
"start": "2026-10-22",
"end": "2026-10-24"
}
]<script lang="ts">
import { Scheduler, type EventInput } from '@sv5ui/scheduler';
// Naive ISO strings are read in the scheduler's timeZone. end is exclusive:
// an all-day event on the 17th ends on the 18th.
let events = $state<EventInput[]>([
{ id: 'planning', title: 'Sprint planning', start: '2026-09-14T10:00', end: '2026-09-14T11:30' },
{ id: 'design', title: 'Design review', start: '2026-09-15T13:00', end: '2026-09-15T14:30' },
{ id: 'offsite', title: 'Team offsite', start: '2026-09-17', end: '2026-09-19', allDay: true }
]);
</script>
<Scheduler bind:events timeZone="Asia/Ho_Chi_Minh" height={520} />
<p>{events.length} events</p>2. Pick a view
view and date are bindable, so the toolbar and controls of your own move the same state. date is a ZonedDateTime, which is why the step imports now from @internationalized/date.
October 2026
<script lang="ts">
import { now } from '@internationalized/date';
import { Scheduler, type EventInput } from '@sv5ui/scheduler';
const zone = 'Asia/Ho_Chi_Minh';
let events = $state<EventInput[]>([/* ... */]);
let view = $state('month'); // new
let date = $state(now(zone)); // new: a ZonedDateTime
</script>
<!-- Both are bindable, so the toolbar and your own controls move the same state -->
<Scheduler bind:events bind:view bind:date timeZone={zone} height={520} />
<Button label="Next month" onclick={() => (date = date.add({ months: 1 }))} />
<Button label="Agenda" onclick={() => (view = 'agenda')} />3. Save changes
onMutate runs after the change is already on screen. Here it waits 600ms and shows a toast; throwing instead would slide the event back. Move two events quickly: each has its own queue. The Saving Changes page covers rollback, conflicts and server-assigned ids.
Oct 19 – 25, 2026
<script lang="ts">
import { toast } from 'sv5ui';
import { Scheduler, type EventInput, type Mutation } from '@sv5ui/scheduler';
let events = $state<EventInput[]>([/* ... */]);
// Called after the change is already on screen. Throw to roll it back.
async function onMutate(mutation: Mutation) { // new
await fetch('/api/events', { method: 'POST', body: JSON.stringify(mutation) });
toast(`Saved ${mutation.kind} of ${mutation.after?.title ?? mutation.before?.title}`);
}
</script>
<Scheduler bind:events timeZone="Asia/Ho_Chi_Minh" height={520} {onMutate} />4. Group into calendars
Events join a calendar through calendarId and take its colour. sidebar adds the date navigator, search and a checklist bound to hiddenCalendars. Untick Personal in the sidebar and hiddenCalendars updates.
Oct 19 – 25, 2026
<script lang="ts">
import { Scheduler, type EventInput, type SchedulerCalendar } from '@sv5ui/scheduler';
const calendars: SchedulerCalendar[] = [ // new
{ id: 'work', title: 'Work', color: 'primary' },
{ id: 'personal', title: 'Personal', color: 'success' }
];
// Events join a calendar through calendarId and take its colour
let events = $state<EventInput[]>([
{ id: 'planning', title: 'Sprint planning', calendarId: 'work', start: '...', end: '...' },
{ id: 'gym', title: 'Gym', calendarId: 'personal', start: '...', end: '...' }
]);
let hiddenCalendars = $state<string[]>([]); // new
</script>
<!-- sidebar adds a date navigator, search and the calendar checklist -->
<Scheduler bind:events bind:hiddenCalendars {calendars} sidebar timeZone="Asia/Ho_Chi_Minh" height={560} />5. Your own form
With createPanel set the scheduler never creates an event by itself. Drag across empty slots or press Enter on a focused slot and your snippet opens with the picked range; create() saves through the same pipeline as a drag. A full form with a calendar picker, dates and notes is on the Creating & Editing page.
Oct 19 – 25, 2026
<script lang="ts">
import { Button, Input } from 'sv5ui';
import { Scheduler, type EventInput } from '@sv5ui/scheduler';
let events = $state<EventInput[]>([/* ... */]);
let title = $state('');
</script>
<!-- With createPanel set, dragging across empty slots opens your form
instead of creating "New event". create() saves through onMutate. -->
<Scheduler bind:events timeZone="Asia/Ho_Chi_Minh" height={520} createPanel={form} />
{#snippet form({ start, end, allDay, close, create })}
<Input bind:value={title} placeholder="What is it about?" autofocus />
<div class="flex gap-2">
<Button label="Save" onclick={() => { create({ title, start, end, allDay }); title = ''; }} />
<Button label="Cancel" variant="ghost" color="surface" onclick={close} />
</div>
{/snippet}Where to Next
Props
The props the five steps use.
| Prop | Type | Default |
|---|---|---|
events | EventInput<T>[] | - |
calendars | SchedulerCalendar[] | [] |
onMutate | (m: Mutation<T>) => MutationResult<T> | Promise<...> | - |
sidebar | boolean | Snippet<[SidebarSnippetProps<T>]> | false |
timeZone | TimeZoneId | local zone |
height | number | string | - |
createPanel | Snippet<[CreatePanelSnippetProps<T>]> | - |
Snippets
What the createPanel snippet in step 5 receives.
| Type | Fields |
|---|---|
CreatePanelSnippetProps | start, end, allDay, close, create |