Interaction

Creating & Editing

Every app stores something different, so the form is yours. The scheduler provides the flow: it knows when the user picked a place for a new event, opens a panel there, and saves whatever your form builds through the same pipeline as a drag.

Three Ways In

Which one

WayOpens when
createPanelA drag over empty slots, Enter on a focused slot, or setting draft
onSelectSlot and onEventClickA click on a slot, a click on an event
NeitherA drag creates "New event" at once
<!-- 1. A panel inside the calendar, filled by your snippet -->
<Scheduler bind:events createPanel={form} />

<!-- 2. Your own dialog, opened from the two reports -->
<Scheduler bind:events creatable={false} detail={false}
  onSelectSlot={(slot) => openDialog(slot)}
  onEventClick={(event) => openDialog(event)} />

<!-- 3. Nothing: a drag creates "New event", to be renamed later -->
<Scheduler bind:events />

A Create Panel

Pass createPanel and a panel slides in from the side of the calendar when the user marks out a new event: a drag over empty slots, or Enter on a focused slot. The snippet receives the picked range and a create function. While createPanel is set the scheduler never creates an event on its own. The form below is built from sv5ui inputs and is not part of the package. Click an existing event, then Open details, to edit it in the same panel.

Oct 19 – 25, 2026

GMT+7
Mon 19
Tue 20
Wed 21
Thu 22
Fri 23
Sat 24
Sun 25
All day
Lunch break

onMutate calls

  • Create, edit or delete something.
<script lang="ts">
  import { Scheduler, type CreatePanelSnippetProps, type EventInput } from '@sv5ui/scheduler';
  import EventForm from './EventForm.svelte';

  interface Notes { notes: string }
  let events = $state<EventInput<Notes>[]>([/* ... */]);
</script>

<!-- Drag across empty slots, or press Enter on a focused slot, and the panel
     opens with the picked range. create() goes through onMutate like a drag:
     the id may be replaced by the server, a failure rolls back. -->
<Scheduler bind:events {calendars} createPanel={form} onMutate={save} />

{#snippet form({ start, end, allDay, close, create }: CreatePanelSnippetProps<Notes>)}
  <EventForm {start} {end} {allDay} {calendars} onSubmit={create} onCancel={close} />
{/snippet}

CreatePanelSnippetProps

FieldDescription
start, endThe picked range, snapped to the grid; end exclusive
allDayWhole days were picked rather than a time range
close()Closes the panel without creating anything
create(input)Creates through the pipeline and closes. id is generated when omitted; onMutate may replace it

Opening It From a Button

draft is the range the panel shows, or null when it is closed. It is bindable: set it to open the panel from a button of your own, as the "Create event" button in the toolbar above does, and read it to know what is being planned. With creatable={false} the grid never opens the panel and draft becomes the only way in.

<script lang="ts">
  import { now } from '@internationalized/date';
  let draft = $state<SlotSelection | null>(null);

  function nextHour() {
    const start = now(zone).set({ minute: 0, second: 0, millisecond: 0 }).add({ hours: 1 });
    draft = { start, end: start.add({ hours: 1 }), allDay: false };
  }
</script>

<!-- draft is the range the panel shows; set it to open the panel from anywhere,
     read it to know what is being planned. null closes it. -->
<Scheduler bind:events bind:draft createPanel={form} toolbarActions={actions} />

{#snippet actions()}
  <Button label="Create event" icon="lucide:plus" onclick={nextHour} />
{/snippet}

<!-- creatable={false} with a createPanel: the grid never opens it; only draft does -->

A Click Only Reports

A click on an empty slot does not open the panel: it reports one slot, or one day, through onSelectSlot. Opening a form on every stray click is the behaviour most apps end up switching off, so it is not the default. If a click should open something, open it yourself from that callback.

<!-- A click on an empty slot only reports it. It does not open the panel, so a
     stray click does not throw a form at the user. -->
<Scheduler
  bind:events
  createPanel={form}
  onSelectSlot={(slot) => console.log(slot.start.toString(), slot.end.toString(), slot.allDay)}
/>

<!-- To open your own dialog on a click instead -->
<Scheduler bind:events creatable={false} onSelectSlot={(slot) => (dialog = slot)} />
<Modal open={dialog !== null} title="New event">...</Modal>

Your Own Dialog Instead

Leave createPanel out, turn the built-in details off, and listen to the two reports. Saving from a dialog is not a gesture, so onMutate does not run: save to your API, then write into the array. Click a slot or an event below.

Oct 19 – 25, 2026

GMT+7
Mon 19
Tue 20
Wed 21
Thu 22
Fri 23
Sat 24
Sun 25
All day
Lunch break
<script lang="ts">
  let editing = $state<SchedulerEvent | SlotSelection | null>(null);

  async function saveDialog(values: EventInput) {
    // Not a gesture, so onMutate does not run: save, then update the array
    const saved = await api.save(values);
    const index = events.findIndex((event) => event.id === saved.id);
    if (index === -1) events.push(saved);
    else events[index] = saved;
    editing = null;
  }
</script>

<Scheduler
  bind:events
  creatable={false}
  detail={false}
  onSelectSlot={(slot) => (editing = slot)}
  onEventClick={(event) => (editing = event)}
/>

<Modal open={editing !== null} onOpenChange={(open) => !open && (editing = null)}>
  <!-- your form, seeded from editing -->
</Modal>

Event Details

detail decides what a click on an event opens. popover is a card beside the event with the date, time, calendar, a delete button when the event can be deleted, and a close button. slideover is a panel inside the calendar with the same content and the delete button in its footer. false opens nothing. Below compactBreakpoint a popover opens as a slide-over. onEventClick fires in every mode.

Oct 19 – 25, 2026

GMT+7
Mon 19
Tue 20
Wed 21
Thu 22
Fri 23
Sat 24
Sun 25
All day
Lunch break
<!-- A card anchored to the event, with delete and close. The default. -->
<Scheduler bind:events detail="popover" />

<!-- A panel inside the calendar; the body is eventPanel when given -->
<Scheduler bind:events detail="slideover" />

<!-- Nothing opens; onEventClick still fires -->
<Scheduler bind:events detail={false} onEventClick={openMine} />

<!-- Below compactBreakpoint a popover opens as a slide-over anyway -->

Adding to the Details

eventDetail adds content under the default details, in the popover and in the slide-over alike. The demo above shows the notes and a button through it.

<!-- Adds to the default details, in the popover and the slide-over alike -->
<Scheduler bind:events eventDetail={extra} />

{#snippet extra({ event, close }: EventDetailSnippetProps<Meeting>)}
  <p class="text-sm">{event.data?.location}</p>
  <Button label="Join call" size="sm" onclick={() => { open(event.data?.link); close(); }} />
{/snippet}

A Panel per Kind of Event

eventPanel replaces the body of the slide-over, keeping its title and close button. With detail="slideover" it opens on click; with detail="popover" the popover gains an Open details button that switches to it, which is what the first demo on this page does.

<!-- Replaces the body of the slide-over. With detail="popover" the popover gains
     an Open details button that switches to this panel. -->
<Scheduler bind:events detail="popover" eventPanel={panel} />

{#snippet panel({ event, close, remove, update, deletable }: EventPanelSnippetProps<Notes>)}
  <EventForm
    start={event.start}
    end={event.end}
    allDay={event.allDay ?? false}
    initial={{ title: event.title, calendarId: event.calendarId, data: event.data }}
    {calendars}
    onSubmit={(changes) => { update(changes); close(); }}
    onCancel={close}
  />
  {#if deletable}
    <Button label="Delete" color="error" variant="soft" onclick={remove} />
  {/if}
{/snippet}

EventPanelSnippetProps

FieldDescription
eventThe event, with ZonedDateTime dates and your data
close()Closes the panel
remove()Deletes through the pipeline and closes. Does nothing when deletable is false
update(changes)Merges the fields and sends an update mutation. Does nothing when deletable is false
deletableWhether the event may be changed, following editable on the event and the scheduler

Editing an Existing Event

update(changes) from the panel merges the fields you pass into the event and sends an update mutation, so onMutate runs, a failure rolls back and the change is announced. A change to data alone is sent too. Occurrences of a series cannot be updated; change the rule on the series as Recurring Events shows.

// update(changes) merges the fields into the event and sends an 'update'
// mutation through the pipeline: onMutate runs, a failure rolls back, the
// change is announced. Fields left out keep their value. It does nothing
// when deletable is false, which follows editable on the event and the scheduler.
update({ title: 'Design review, extended', end: event.end.add({ minutes: 30 }) });
update({ data: { ...event.data, notes: 'Bring the mockups' } });   // payload only is fine

// An occurrence of a series is never editable; change the rule on the series instead.

onEventClick

<!-- Fires on click or keyboard activation, whatever detail is, once per opening
     click (not again for the click that closes a popover). An occurrence arrives
     with seriesId set. -->
<Scheduler bind:events onEventClick={(event) => analytics.track('open', event.id)} />

Props

The props of the three ways in. draft is bindable.

PropTypeDefault
creatablebooleantrue
editablebooleantrue
detail'popover' | 'slideover' | false'popover'
eventDetailSnippet<[EventDetailSnippetProps<T>]>-
eventPanelSnippet<[EventPanelSnippetProps<T>]>-
createPanelSnippet<[CreatePanelSnippetProps<T>]>-
draftSlotSelection | nullnull
onEventClick(event: SchedulerEvent<T>) => void-
onSelectSlot(selection: SlotSelection) => void-

Snippets

What the snippets on this page receive. Every type is exported from @sv5ui/scheduler.

TypeFields
EventDetailSnippetPropsevent, close
EventPanelSnippetPropsevent, close, remove, update, deletable
CreatePanelSnippetPropsstart, end, allDay, close, create