Selecting cells & events

Drag empty slots to create events (drag across days for a rectangle - one per day); Ctrl/Cmd-click events to multi-select (Shift-click a range), drag the set to move it together, or press Delete to remove. Enterprise scheduler view. (requires @svgrid/enterprise)

A live, editable Svelte 5 data grid example from the SvGrid gallery (Scheduler). See the SvGrid documentation for the full API.

About this example

Three kinds of selection on the Svelte 5 scheduler. rangeSelectable lets you drag across empty slots to mark a continuous date and time range, or across days for a multi-day span, and press Enter or choose Add Event to create one event from it, with onRangeSelect delivering the start and end. eventSelectable lets you Ctrl or Cmd-click events to multi-select, Shift-click for a range, drag one to move the set together and press Delete to remove them.

Three kinds of selection on the same scheduler:

  • rangeSelectable - drag across empty slots to MARK a continuous date/time range (drag across days for a multi-day span), then press Enter or right-click -> "Add Event" to create ONE event from it. onRangeSelect gives the start/end datetimes. Selecting alone never creates anything.
  • eventSelectable - Ctrl/Cmd-click events to multi-select (Shift-click for a range); the set moves together when you drag one, and Delete removes them.

Imports, features and API used

Imports: @svgrid/grid, @svgrid/enterprise

Columns: title (Title), start (Start), end (End)

Frequently asked questions

Does selecting a range create an event?

No. Selecting only marks the range; creation happens when you press Enter or pick Add Event, which calls onRangeSelect with the start and end so your code adds the row.

How do multi-selected events move?

Dragging any selected event moves all of them by the same offset; onEventMove is called per event, so each row gets its own new times.

What does onEventSelectionChange report?

The ids of the currently selected events after every click, so a toolbar can enable bulk actions for the set.

Related documentation

Source code (372-scheduler-selection.svelte)

<script lang="ts">
  /**
   * 372. Selecting cells & events
   * -----------------------------
   * Three kinds of selection on the same scheduler:
   *   - `rangeSelectable` - drag across empty slots to MARK a continuous
   *     date/time range (drag across days for a multi-day span), then press Enter
   *     or right-click -> "Add Event" to create ONE event from it. `onRangeSelect`
   *     gives the `start`/`end` datetimes. Selecting alone never creates anything.
   *   - `eventSelectable` - Ctrl/Cmd-click events to multi-select (Shift-click for
   *     a range); the set moves together when you drag one, and Delete removes them.
   */
  import {
    SvGrid,
    type ColumnDef,
    type SchedulerRangeSelection,
    type SchedulerEventMoveEvent,
    type SchedulerEventResizeEvent,
    type SchedulerEventCommitEvent,
  } from '@svgrid/grid'
  import { enableSchedulerView, setLicenseKey } from '@svgrid/enterprise'

  setLicenseKey('SVENTERPRISE-DEV-LOCAL')
  enableSchedulerView()

  type Ev = { id: number; title: string; start: string; end: string; color: string; allDay?: boolean }

  const monday = new Date()
  monday.setDate(monday.getDate() - ((monday.getDay() + 6) % 7))
  monday.setHours(0, 0, 0, 0)
  const pad = (n: number) => String(n).padStart(2, '0')
  const iso = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
  const at = (day: number, h: number, m = 0) => {
    const d = new Date(monday)
    d.setDate(monday.getDate() + day)
    d.setHours(h, m, 0, 0)
    return iso(d)
  }
  const PALETTE = ['#4f46e5', '#0891b2', '#d97706', '#16a34a', '#db2777']

  let seq = 100
  let rows = $state<Ev[]>([
    { id: 1, title: 'Standup', start: at(0, 9, 0), end: at(0, 9, 30), color: PALETTE[0] },
    { id: 2, title: 'Review', start: at(1, 10, 0), end: at(1, 11, 0), color: PALETTE[1] },
    { id: 3, title: 'Focus', start: at(2, 13, 0), end: at(2, 15, 0), color: PALETTE[2] },
    { id: 4, title: 'Sync', start: at(3, 11, 0), end: at(3, 12, 0), color: PALETTE[3] },
  ])

  const columns: ColumnDef<any, Ev>[] = [
    { field: 'title', header: 'Title', editorType: 'text', width: 200 },
    { field: 'start', header: 'Start', editorType: 'datetime', width: 170 },
    { field: 'end', header: 'End', editorType: 'datetime', width: 170 },
  ]

  let view = $state<'calendar' | 'table'>('calendar')
  let selectedCount = $state(0)
  let lastRange = $state('')

  const make = (start: Date, end: Date): Ev => ({ id: ++seq, title: 'New', start: iso(start), end: iso(end), color: PALETTE[seq % PALETTE.length] })

  // A range select is a CONTINUOUS date/time span - create ONE event from it.
  // Dragging the all-day row makes an all-day event; the hourly grid a timed one.
  function onRangeSelect(sel: SchedulerRangeSelection) {
    if (sel.allDay) {
      rows = [...rows, { id: ++seq, title: 'New', start: iso(sel.start), end: iso(sel.end), color: PALETTE[seq % PALETTE.length], allDay: true }]
      lastRange = `all-day · ${sel.days.length} day(s)`
      return
    }
    rows = [...rows, make(sel.start, sel.end)]
    lastRange = `${Math.round((sel.end.getTime() - sel.start.getTime()) / 60000)} min`
  }
  function onEventSelectionChange(sel: Ev[]) {
    selectedCount = sel.length
  }
  // Double-clicking an empty slot creates an event there.
  function onEventAdd(start: Date, end: Date) {
    rows = [...rows, make(start, end)]
  }
  function onEventMove(e: SchedulerEventMoveEvent<Ev>) {
    e.row.start = iso(e.start)
    e.row.end = iso(e.end)
    e.row.allDay = e.allDay // dragging to/from the all-day row flips this
  }
  function onEventResize(e: SchedulerEventResizeEvent<Ev>) {
    e.row.start = iso(e.start)
    e.row.end = iso(e.end)
  }
  function onEventCommit(e: SchedulerEventCommitEvent<Ev>) {
    Object.assign(e.row, e.values)
  }
  function onEventDelete(row: Ev) {
    rows = rows.filter((r) => r !== row)
  }
</script>

<section class="flex flex-col flex-1 min-h-0 gap-3">
  <div class="flex items-center justify-between gap-3 shrink-0">
    <div class="sc-lead text-sm">
      <strong>Drag</strong> (or <strong>click</strong>) empty slots to mark a
      <strong>date/time range</strong>; <strong>arrow keys</strong> move the cell,
      <strong>Shift+arrows</strong> extend it. Press <strong>Enter</strong> or
      right-click → <strong>Add Event</strong> to create it (<strong>Esc</strong>
      cancels). <strong>Ctrl/Cmd-click</strong> events to multi-select (Shift-click
      a range); drag one to move them together, or <strong>Delete</strong> to remove.
    </div>
    <div class="sc-seg inline-flex rounded-md overflow-hidden text-sm shrink-0">
      <button class="sc-seg-btn px-3 py-1 {view === 'calendar' ? 'sc-on' : ''}" onclick={() => (view = 'calendar')}>Calendar</button>
      <button class="sc-seg-btn px-3 py-1 {view === 'table' ? 'sc-on' : ''}" onclick={() => (view = 'table')}>Table</button>
    </div>
  </div>

  <div class="flex-1 min-h-0">
    {#if view === 'calendar'}
      <SvGrid
      columnResize
        data={rows}
        columns={columns}
        getRowId={(r) => String(r.id)}
        containerHeight="100%"
        scheduler={{
          startField: 'start',
          endField: 'end',
          allDayField: 'allDay',
          titleField: 'title',
          colorField: 'color',
          initialView: 'week',
          weekStartsOn: 1,
          dayStartHour: 0,
          dayEndHour: 24,
          editable: true,
          drawer: true,
          history: true,
          rangeSelectable: true,
          eventSelectable: true,
          onRangeSelect,
          onEventSelectionChange,
          onEventAdd,
          onEventMove,
          onEventResize,
          onEventCommit,
          onEventDelete,
        }}
      />
    {:else}
      <SvGrid
      columnResize data={rows} columns={columns} getRowId={(r) => String(r.id)} sortable enableInlineEditing rowHeight={36} containerHeight="100%" fitColumns />
    {/if}
  </div>

  <footer class="sc-foot text-sm shrink-0">
    {rows.length} events · {selectedCount} selected{lastRange ? ` · last drag: ${lastRange}` : ''}
  </footer>
</section>

<style>
  .sc-lead { color: var(--sg-fg, #475569); }
  .sc-foot { color: var(--sg-muted, #64748b); }
  .sc-seg { border: 1px solid var(--sg-border, #cbd5e1); }
  .sc-seg-btn { background: transparent; color: var(--sg-fg, #0f172a); }
  .sc-seg-btn:hover { background: var(--sg-row-hover-bg, #f1f5f9); }
  .sc-seg-btn.sc-on { background: var(--sg-accent, #1e293b); color: var(--sg-on-accent, #fff); }
</style>

View this example on GitHub

More Scheduler examples

  • Scheduler / calendar mode - The same <SvGrid>, same data + columns, rendered as a full calendar by setting one `scheduler` prop: rows become events bucketed by time. Switch Month / Week / Day / Agenda in the toolbar; drag an event to reschedule, drag its edge to resize, click to edit in a drawer. onEventMove / onEventResize write the new times back onto your row. Toggle Calendar / Table to see it is one grid. The calendar renderer ships in @svgrid/enterprise.
  • Timeline views (Day / Week) - A horizontal resource timeline: teams are rows, time runs left→right on a scrollable axis, work items are bars sized to real durations (half-day blocks up to multi-day tasks). Switch Day (hour ticks) or Week (day ticks); drag a bar to re-time it, to another team's row to reassign, or an edge to resize. Click / drag empty space to select a range (arrows move, Shift+arrows extend, Enter creates); Ctrl-click bars to multi-select. Bar fill = team, left strip = status.
  • Horizon - Outlook-style calendar app - A full calendar client on the Scheduler + SvDockManager: NavPane module rail + mini date-picker + calendar toggles, and a dockable workspace (drag / resize / float panes) with the calendar over an Upcoming grid and an Inbox. Recurring events with per-occurrence edits, a second time-zone ruler, iCal import/export and undo.
  • Meridian Clinic - appointment scheduling - A front-desk appointment console: providers with their own hours as day columns, a drag-in patient waitlist, a utilization gauge, and a live patient-flow pipeline (Waiting / Checked in / In room / Completed) that updates as you work.
  • Dispatch - field-service operations board - A map-first field-service operations console: a live ops map with technician pins by status and job markers, an unassigned queue you drag onto the crew timeline, and SLA countdowns. No double-booking.