Drag & Drop
Drag an event to move it, pull its edge to resize it, drag across empty slots to create one, and drop items from anywhere on the page. Each gesture ends in one mutation, so saving is the same function whatever the gesture.
Move and Resize
Every gesture goes through onMutate and is logged below. The board meeting today is locked with editable: false, so it has no grab cursor and does not move. Switch the two props off to feel the
difference.
Oct 19 – 25, 2026
onMutate calls
- Move, resize, create or delete something.
<!-- Nothing to switch on. Drag the body of an event to move it, drag its edge to
resize it. Every gesture ends in one mutation. -->
<Scheduler bind:events onMutate={(mutation) => api.save(mutation)} />What a Drop Does
The result depends on what was picked up and where it lands. Times snap to slotMinutes. A month cell keeps a timed event timed; the all-day row of the week view turns it
all-day. Resizing has a floor of one slot or one day, and a bar cannot be resized at the
edge where it continues into another row.
Drops
| Picked up | Dropped on | Result |
|---|---|---|
Timed event | Time slot | Keeps its length; start snaps to the slot |
Timed event | Month cell | Changes day, keeps its clock time |
Timed event | All-day row | Becomes all-day, covering the days it spanned |
All-day event | Time slot | Becomes timed, two slots long |
All-day event | Whole-day cell | Shifts by whole days |
Edge of a timed event | Slot | New start or end, at least one slot |
Edge of a timed event | Day cell | That edge changes day and keeps its time |
Edge of a bar | Day cell | New first or last day, at least one day |
// What a drop does, by where the event came from and where it lands
//
// timed event -> time slot keeps its length, start snaps to the slot (move)
// timed event -> month cell changes day, keeps its clock time (move)
// timed event -> all-day row becomes all-day, covering the days it spanned (move)
// all-day event -> time slot becomes timed, two slots long (move)
// all-day event -> whole-day cell shifts by whole days (move)
//
// edge of a timed event -> slot new start or end, at least one slot (resize)
// edge of a timed event -> day cell changes the day of that edge, keeps its time (resize)
// edge of a bar -> day cell new first or last day, at least one day (resize)Creating by Dragging
Drag across empty slots, or across days in the month view, and an event called labels.newEvent is created where you let go. A single click never creates anything; it reports the slot through onSelectSlot. When a createPanel is set the same drag opens your form instead; see Creating & Editing.
<!-- Drag across empty slots and an event named labels.newEvent is created, as a
'create' mutation. A plain click creates nothing; it reports the slot. -->
<Scheduler
bind:events
onMutate={(mutation) => mutation.kind === 'create' ? api.create(mutation.after) : api.save(mutation)}
onSelectSlot={(slot) => console.log('clicked', slot.start.toString())}
/>
<!-- With a createPanel the same drag opens your form instead: see Creating & Editing -->Snapping and Autoscroll
A drag needs three pixels of movement to start, so a click stays a click. Times snap to the nearest slot boundary. When the pointer nears the top or bottom edge of a scrolling grid, the grid scrolls on its own, faster the closer to the edge. Escape cancels the gesture and puts the event back.
Touch
On a touch screen a drag starts after a press of 300 milliseconds, so a swipe scrolls the page and a rest picks up the event. While the press is held, the context menu is suppressed. A quick tap does what a click does.
Turning It Off
Two props and two event fields. Locked events lose their grab and resize cursors and the delete button in their details.
Locks
| Setting | Turns off |
|---|---|
creatable={false} | Drag to create, Enter to create |
editable={false} | Move, resize, delete and update of every event |
editable: false on an event | Move, resize, delete and update of that event |
background: true | Everything, always |
An occurrence of a series | Everything, always |
<!-- No creating by drag or Enter. Drops from dragSource still create. -->
<Scheduler bind:events creatable={false} />
<!-- No moving, resizing, deleting or updating, for every event -->
<Scheduler bind:events editable={false} />
<!-- One event locked; the rest as usual -->
{ id: 'board', title: 'Board meeting', start: '...', end: '...', editable: false }
<!-- Always locked: background events, and occurrences of a series -->Drop From Your Own List
dragSource is an attachment that turns any element into something that can be dropped on a grid. It takes
a function returning the event's data, called when the drag starts. Drop on a time slot for a
timed event of durationMinutes; drop on a month cell or the all-day row for an all-day event. The cards below are plain
buttons.
Oct 19 – 25, 2026
<script lang="ts">
import { dragSource, type DragSourceData } from '@sv5ui/scheduler';
const tasks: DragSourceData[] = [
{ title: 'Write release notes', durationMinutes: 60, calendarId: 'work' },
{ title: 'Review pull requests', durationMinutes: 90, calendarId: 'team' },
{ title: 'Renew passport', calendarId: 'personal' } // no length: two slots
];
</script>
<!-- Any element. The attachment reads the data when the drag starts. -->
{#each tasks as task (task.title)}
<button {@attach dragSource(() => task)}>{task.title}</button>
{/each}
<Scheduler bind:events onMutate={save} />DragSourceData
| Field | Description |
|---|---|
title, calendarId, color, data, allDay, editable, background | Copied onto the created event |
id | Optional. Generated when omitted; the server may still replace it |
durationMinutes | Length on a time slot. Two slots when omitted. Ignored on a whole-day cell |
// Everything an event has except its dates and its rule, plus a length
type DragSourceData<T> = Omit<EventInput<T>, 'id' | 'start' | 'end' | 'recurrence'> & {
id?: string; // generated when omitted; onMutate may still replace it
durationMinutes?: number; // length when dropped on a time slot; two slots when omitted
};
// Dropped on a time slot: a timed event of durationMinutes, start snapped to the slot.
// Dropped on a month cell or the all-day row: an all-day event of one day.
// Either way a 'create' mutation with before: null.One List, Several Schedulers
A drag source belongs to the page, not to a scheduler. Drop the same card on either calendar below.
Linh
October 20, 2026
October 20, 2026
Today GMT+7Minh
October 20, 2026
October 20, 2026
Today GMT+7<!-- A drag source is not tied to a scheduler. Drop it on any grid on the page. -->
<button {@attach dragSource(() => ({ title: 'Handover', durationMinutes: 30 }))}>Handover</button>
<Scheduler bind:events={linh} />
<Scheduler bind:events={minh} />Refusing a Drop
There is no prop that forbids a slot. Decide in onMutate, where the whole event after the change is in hand: throw, and the event slides back while onError tells the user why. Below, anything dropped outside 9:00 to 18:00 is refused. An interaction plugin can refuse during the drag instead, when the feedback has to be immediate.
Oct 19 – 25, 2026
<!-- There is no rule prop. Refuse in onMutate: the event slides back and onError
runs. The check sees the whole event after the change, so overlaps, hours
and permissions are all one kind of check. -->
<Scheduler
bind:events
onMutate={(mutation) => {
const event = mutation.after;
if (!event) return api.remove(mutation.eventId);
if (event.start.hour < 9 || event.end.hour > 18) throw new Error('Outside opening hours');
if (overlapsAnother(event, events)) throw new Error('That slot is taken');
return api.save(mutation);
}}
onError={(mutation, error) => toast.error(String(error))}
/>The Cursor and the DOM
An event that can be moved shows a grab cursor, and its resize handles show a resize cursor
through data-sch-edge. While a gesture runs the grid carries data-sch-gesture, and a drag source element carries data-sch-dragging until it is dropped, which is how the cards above fade.
<!-- The grid sets data-sch-gesture while a gesture runs, the event box hovers a
data-sch-edge over its resize handles, and the source element carries
data-sch-dragging while it is being dragged. -->
<button
class="rounded border px-3 py-2 data-[sch-dragging]:opacity-50"
{@attach dragSource(() => task)}
>
{task.title}
</button>
<style>
.planner :global([data-sch-gesture='move'] [data-sch-day-index]) { outline: 1px dashed var(--color-primary); }
</style>Props
The props that govern gestures.
| Prop | Type | Default |
|---|---|---|
creatable | boolean | true |
editable | boolean | true |
dragSources | DragSourceData<T>[] | [] |
onMutate | (m: Mutation<T>) => MutationResult<T> | Promise<...> | - |
slotMinutes | number | 30 |
onSelectSlot | (selection: SlotSelection) => void | - |