Scheduler / calendar mode

Set one scheduler prop and the grid renders its rows as events on a calendar instead of a table. It is the same <SvGrid>, the same data and columns - only the presentation changes. Events are placed by time across four views (Month, Week, Day, Agenda); dragging an event to a new day or time tells you to reassign its start/end on your own data.

Like Kanban board mode, the scheduler is a pure view of the grid: it renders the grid's already filtered, sorted, and searched rows and writes back only through callbacks - it never mutates your data.

Enterprise feature. The scheduler prop and its config types are part of the free grid, but the calendar renderer ships in @svgrid/enterprise. Register it once and the view lights up:

import { setLicenseKey, enableSchedulerView } from '@svgrid/enterprise'
setLicenseKey('YOUR-KEY')   // omit to run soft-gated with a watermark
enableSchedulerView()

installEnterprise(api) also calls enableSchedulerView() for you. Without the renderer registered, a grid with a scheduler prop shows an upgrade note.

Open the live example: Scheduler / calendar mode (Scheduler)

Every example below runs against this setup. enableSchedulerView() registers the enterprise renderer; the scheduler prop itself is part of the free grid.

<script lang="ts">
  import { SvGrid, type GridColumns, type RecurrenceRule } from '@svgrid/grid'
  import { enableSchedulerView } from '@svgrid/enterprise'

  enableSchedulerView()

  // One row type wide enough for every example on this page - resources,
  // recurrence, per-event colour and the booking-rule samples all read from it.
  type Event = {
    id: number
    title: string
    start: string
    end: string
    allDay?: boolean
    color?: string
    staffColor?: string
    roleColor?: string
    room?: string
    machine?: string
    provider?: string
    team?: string
    assignees?: string[]
    repeat?: RecurrenceRule | RecurrenceRule[] | null
    exceptions?: unknown[]
  }

  // Anchored on today, so the calendar always opens with events in view.
  const at = (dayOffset: number, hour: number, mins = 0) => {
    const d = new Date()
    d.setDate(d.getDate() + dayOffset)
    d.setHours(hour, mins, 0, 0)
    const p = (n: number) => String(n).padStart(2, '0')
    return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`
  }

  let data = $state<Event[]>([
    { id: 1, title: 'Standup',     start: at(0, 9),  end: at(0, 9, 15),  color: '#3b82f6', room: 'Aurora',  team: 'Platform' },
    { id: 2, title: 'Design review', start: at(0, 11), end: at(0, 12),   color: '#8b5cf6', room: 'Borealis', team: 'Design' },
    { id: 3, title: 'Team offsite', start: at(1, 0),  end: at(3, 0),     allDay: true,     color: '#22c55e', team: 'Platform' },
    { id: 4, title: '1:1',         start: at(2, 15), end: at(2, 15, 30), color: '#f59e0b', room: 'Aurora',  team: 'Design' },
  ])

  const rows = data
  const iso = at
  const makeEvent = (start: Date, end: Date): Event => ({
    id: Math.max(0, ...data.map((e) => e.id)) + 1,
    title: "New event",
    start: at(0, start.getHours(), start.getMinutes()),
    end: at(0, end.getHours(), end.getMinutes()),
  })
  const rooms = [
    { id: "Aurora", label: "Aurora" },
    { id: "Borealis", label: "Borealis" },
  ]
  const resources = rooms
  const tasks = data
  let selected = $state<Event | null>(null)
  const initials = (name: string) =>
    name.split(" ").map((w) => w[0]).join("").slice(0, 2).toUpperCase()

  const columns: GridColumns<Event> = [
    { field: 'title', header: 'Event', width: 220 },
    { field: 'start', header: 'Start', width: 170 },
    { field: 'end',   header: 'End',   width: 170 },
  ]
</script>

The minimum

Point scheduler.startField at the field that holds each event's start. That is the only required option. With an endField too, events get their real duration; without one they use defaultDurationMin (60 by default). The title defaults to the first column's field.

<SvGrid {data} {columns} scheduler={{ startField: 'start', endField: 'end' }} />

Start and end values may be a Date, an epoch number, or a parseable/ISO string - the same coercion the date pickers use.

Views

The toolbar offers Month, Week, Day, and Agenda. Restrict or reorder them with views, and pick the one shown first with initialView. Open on a specific date with initialDate (defaults to today).

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end',
    views: ['week', 'day', 'agenda'],
    initialView: 'week',
    weekStartsOn: 1,        // Monday
    dayStartHour: 7,        // time-grid band 07:00..20:00
    dayEndHour: 20,
    slotMinutes: 30,        // snap granularity
  }} />

Colliding (overlapping) events

When events overlap in the time-grid, collisionMode decides how they're shown:

<SvGrid {data} {columns}
  scheduler={{ startField: 'start', endField: 'end', collisionMode: 'cap', maxColumns: 3 }} />

The Month view uses the same idea vertically: a day cell shows its first few events, then a +N more button that opens the day's full list in a popover.

Titles, colors

titleField sets the event label; colorField gives each event an accent color (any CSS color), or set one color for all of them.

<SvGrid {data} {columns}
  scheduler={{ startField: 'start', endField: 'end', titleField: 'title', colorField: 'color' }} />

Add secondaryColorField to encode a second dimension: it paints a strip on the left edge of each event, distinct from the main fill. A shift roster, for instance, can fill each event with the person's color (matching the resource legend) while the left strip shows the role:

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end',
    colorField: 'staffColor',       // fill = person
    secondaryColorField: 'roleColor', // left strip = role
  }} />

Drag to move, resize

Set editable to turn on drag-and-drop and resizing in the time-grid (and drag-to-another-day in Month). An editable event shows a grab cursor; hovering it reveals resize grips at its top and bottom edges. Drag the body to move it, the bottom grip to change the end, or the top grip to change the start. The scheduler computes the new time itself and applies it as an overlay, then fires a notification so you can mirror it onto your row or persist it. Moves and resizes snap to slotMinutes, and dragging across columns (days or resources) also reassigns the day / resource.

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end', editable: true,
    onEventMove: (e) => { e.row.start = iso(e.start); e.row.end = iso(e.end) },
    onEventResize: (e) => { e.row.end = iso(e.end) },
  }} />

In the Month view an editable event can be moved (drag the bar to another day) and resized (hover to reveal left/right grips, then drag an edge across days to change its start or end date, keeping the time-of-day) - the bar grows/shrinks live as you drag, and the day under the cursor highlights. The all-day spanning bars in Week / Day view resize the same way: drag a bar's left or right edge across day columns to change how many days it covers.

A focused event can also be nudged with the keyboard: arrow left/right move it a day, arrow up/down a slot, and Enter opens its editor.

Open the live example: Meridian Clinic - appointment scheduling (Scheduler)

Resources (group by person / room)

Point resourceField at a field and every time-grid view groups by resource - the Day view shows one column per resource, and the Week view groups each resource across all seven days (a spanning group-header row sits above the columns; the grid scrolls horizontally when there are many). Pass an explicit resources array to fix their order, titles, and colors, or let them derive from the data. Dragging an event across columns reports both the new time and the new resource (e.toResource).

A resource legend appears in every view (Month and Agenda included): each chip is a colour key that toggles that resource's events on or off.

Set groupByDate: true to flip the grouping to date-major (each day groups the resources) instead of the default resource-major (each resource groups the days) - the same choice as Smart's groupByDate.

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end',
    resourceField: 'room',
    resources: [
      { id: 'Aurora', title: 'Aurora (12)', color: '#4f46e5' },
      { id: 'Borealis', title: 'Borealis (6)', color: '#0891b2' },
    ],
    initialView: 'week', editable: true,
    // groupByDate: true,   // day -> resources instead of resource -> days
    onEventMove: (e) => { /* e.toResource is the dropped-on room */ },
  }} />

Open the live example: Dispatch - field-service operations board (Scheduler)

Timeline views (resources as rows)

The timeline* views flip the layout horizontal: time runs left→right on a scrollable axis and each resource is a row, with events as horizontal bars lane-packed so overlaps stack. Add any of them to views (and pick one with initialView):

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end',
    resourceField: 'room', resources,
    views: ['timelineDay', 'timelineWeek', 'timelineMonth', 'timelineYear'],
    initialView: 'timelineWeek',
    editable: true,
    onEventMove: (e) => { /* e.toResource = the row it was dropped on */ },
  }} />

Without a resourceField the timeline shows a single All row. When editable, drag a bar sideways to re-time it, drag it to another row to reassign the resource, or drag an edge to resize (day-granular on the multi-day zooms, timelineSlotMinutes on the day zoom). Tune the layout with resourceAreaWidth (left gutter), timelineLaneHeight, and timelineSlotMinutes.

The same rangeSelectable / eventSelectable modes work in the timeline: click or drag empty space to mark a range (a slot in Day, whole days in Week/Month, a month in Year), then arrow keys move the selected cell along the axis (Left/Right) and between resource rows (Up/Down), Shift+arrows extend it, and Enter creates the event (Esc cancels). Ctrl/Cmd-click bars to multi-select, Delete to remove.

The timeline* views cover any horizon. Short-range planning (Day / Week) and the long-range roadmap (Month / Year) read best with data scaled to the zoom, so the demos split them:

Open the live example: Timeline views (Day / Week) (Scheduler)

Open the live example: Portfolio roadmap - initiatives across squads (Scheduler)

Recurring events

A row can carry a recurrenceField holding a RecurrenceRule (or an array of them). The scheduler renders one event per matching day in the visible window - reusing the same pure recurrence engine behind SvCalendar. Each instance keeps the base row's time-of-day and duration.

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end',
    recurrenceField: 'repeat',
  }} />

<!-- a row -->
{ title: 'Daily standup', start: '2026-07-06T09:30', end: '2026-07-06T09:45',
  repeat: { freq: 'weekly', weekdays: [1, 2, 3, 4, 5] } }

The rule shape covers the patterns an enterprise calendar needs:

{ freq: 'daily',   interval: 2 }                              // every other day
{ freq: 'weekly',  weekdays: [1, 3, 5] }                      // Mon / Wed / Fri
{ freq: 'monthly', day: 1 }                                   // the 1st
{ freq: 'monthly', day: -1 }                                  // the last day
{ freq: 'monthly', weekdays: [2], weekOfMonth: 1 }            // the first Tuesday
{ freq: 'monthly', weekdays: [5], weekOfMonth: -1 }           // the last Friday
{ freq: 'yearly',  month: 10, weekdays: [4], weekOfMonth: 4 } // 4th Thursday of Nov
{ freq: 'weekly',  weekdays: [1], count: 8, from: '2026-01-05' } // 8 times, then stop

Editing patterns

Whenever the drawer is on, clicking any event opens a recurrence editor in the drawer - so you can turn a one-off into a recurring event (or drop its pattern) whenever you decide. You don't need a recurrenceField: without one the rule is stored on a default recurrence field (set recurrenceField to name your own). It offers a full, calendar-app-grade set of controls:

Use it to add a pattern to a one-off event, change an existing one, or remove it (set Repeat to "Does not repeat"). Saving writes the rule back through onEventCommit.

Recurring events are also drag- and resize-editable: moving or resizing an occurrence edits the whole series' time-of-day and duration (the pattern's days are kept), so all occurrences shift together.

This event vs. all events (per-occurrence exceptions)

Set a recurrenceExceptionsField (an array on the row) and an onOccurrenceChange handler, and moving / resizing / deleting a single occurrence asks This event or All events. "All events" edits the series as above; This event fires onOccurrenceChange with an exception to store on the row - a moved / retitled / deleted single instance that leaves the rest of the series untouched (like an iCal RECURRENCE-ID / EXDATE).

scheduler={{
  recurrenceField: 'repeat',
  recurrenceExceptionsField: 'exceptions',
  onOccurrenceChange: (e) => {
    // merge e.exception into the row (keyed by e.occurrenceStart)
    const key = (e.exception.occurrenceStart as Date).getTime()
    e.row.exceptions = [
      ...(e.row.exceptions ?? []).filter((x) => new Date(x.occurrenceStart).getTime() !== key),
      { ...e.exception },
    ]
  },
}}

Each exception is { occurrenceStart, deleted? , start?, end?, title? } where occurrenceStart identifies the occurrence (its original start). Without a recurrenceExceptionsField the prompt is skipped and edits apply to the series.

Showing the pattern in a table

The rule stored on a row is a structured object, so in the Table view give the recurrence field a readable column with describeRecurrence - it turns a rule (or list) into a short summary like "Weekly on weekdays", "Every 2 weeks on Fri", or "Monthly on the last Friday":

<script>
  import { describeRecurrence } from '@svgrid/grid'
  const tableColumns = [
    ...columns,
    { id: 'repeat', header: 'Repeat', fieldFn: (r) => describeRecurrence(r.repeat) || '-' },
  ]
</script>

Pass a RecurrenceLabels object as the second argument to localize the weekday / month / ordinal words (and the empty-rule label).

Open the live example: Horizon - Outlook-style calendar app (Scheduler)

Add, edit, delete

The scheduler drives full CRUD back onto your data through callbacks - it never mutates your rows itself:

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end',
    drawer: { title: (row) => row.title, size: '420px' },
    onEventAdd: (start, end) => (rows = [...rows, makeEvent(start, end)]),
    onEventCommit: (e) => Object.assign(e.row, e.values),
    onEventDelete: (row) => (rows = rows.filter((r) => r !== row)),
  }} />

Because it is a view of the grid, pairing it with a plain <SvGrid> table gives you a Calendar / Table toggle over the same rows - every scheduler demo ships one.

Booking rules

Enterprise schedulers enforce when things can be booked. The scheduler shades non-bookable time and can reject double-bookings:

<SvGrid {data} {columns}
  scheduler={{
    resourceField: 'room', resources: rooms,
    businessHours: { start: 9, end: 17 },
    nonWorkingDays: [0, 6],
    shadeUntilNow: true,
    disableConflicts: true,   // enforce no double-booking per room
  }} />

Shading is purely visual unless restrictToBusinessHours is set; disableConflicts is always enforced. Use the pure hasConflict helper to check a placement yourself.

Per-resource availability

businessHours / nonWorkingDays are global, but each resource can declare its own working windows via resource.availability - and they can vary by weekday. Perfect for a clinic where every doctor keeps different hours: each resource's columns shade its own off-hours, and restrictToBusinessHours blocks booking a resource outside its window.

const doctors = [
  { id: 'smith', title: 'Dr. Smith', availability: [
    { days: [1, 3, 5], start: 9, end: 13 },   // Mon/Wed/Fri mornings
    { days: [2, 4], start: 14, end: 18 },      // Tue/Thu afternoons
  ] },
  { id: 'jones', title: 'Dr. Jones', availability: [
    { days: [1, 2, 3, 4, 5], start: 8, end: 12 },
    { days: [1, 2, 3, 4, 5], start: 13, end: 16 },
  ] },
]

An availability window is { start, end } hours with optional days (weekday 0-6; omit for every day). A weekday with no matching window is a full day off. A resource without availability falls back to the global businessHours / nonWorkingDays.

Custom event content & tooltips

Render events however you like with the event snippet (it receives the row), and show rich detail on hover with the tooltip snippet (or tooltip: true for the built-in title + time + resource). tooltipDelay sets the open delay (default 400ms).

<SvGrid {data} {columns}
  scheduler={{ startField: 'start', endField: 'end', event: eventBody, tooltip: tip }} />

{#snippet eventBody(row)}
  <span class="badge">{initials(row.owner)}</span> {row.title}
{/snippet}
{#snippet tip(row)}
  <strong>{row.title}</strong><br />{row.owner} · {row.status}
{/snippet}

The snippet replaces the whole event body (time + title chrome), so you own the layout; resize handles and drag still work around it.

Import / export, undo & clipboard

import { toICS, fromICS } from '@svgrid/grid'
const ics = toICS(rows.map((r) => ({ title: r.title, start: new Date(r.start), end: new Date(r.end), rrule: r.repeat })))
const events = fromICS(pastedText) // -> { title, start, end, allDay, rrule, ... }[]

Unscheduled backlog

Pass unscheduled (a list of { id, title, durationMin?, color? }) and a backlog panel appears beside the Week / Day grid. Drag a task onto a time slot and onSchedule(item, start, resourceId) fires - create the event and drop the task from your list.

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end',
    unscheduled: tasks,           // [{ id, title, durationMin }]
    onSchedule: (item, start) => {
      rows = [...rows, makeEvent(item, start)]
      tasks = tasks.filter((t) => t.id !== item.id)
    },
  }} />

Time zones

Set timeZone (an IANA id like 'America/New_York') and the whole calendar - hour ruler, event positions, day boundaries, all-day grouping and the now-line - is rendered in that zone instead of the browser's. secondaryTimeZones adds read-only "world clock" rulers to the left of the primary gutter.

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end',
    timeZone: 'America/New_York',
    secondaryTimeZones: [{ id: 'Europe/London' }, { id: 'Asia/Tokyo' }],
  }} />

For this to be correct the row start / end must be instant-unambiguous - UTC / offset ISO ('2026-08-01T13:00:00Z', '…+02:00') or epoch ms; a bare local string ('2026-08-01T09:00') is read as the browser instant then shown in the zone. Edits made in the calendar are written back as real instants, so store them offset-aware (e.g. date.toISOString()); the display zone never mutates the stored value - toggle Table on the time-zones demo to see the UTC strings stay put while the calendar shifts. The tz math is DST-aware and uses Intl only (no date library).

Open the live example: Broadcast - content & marketing calendar (Scheduler)

Selecting cells & events

Two selection modes work in every view - time-grid (week / day), month, and timeline. Both are on by default - set rangeSelectable: false or eventSelectable: false to turn one off.

Select a date/time range (rangeSelectable). Click or drag marks a single continuous [start, end] datetime range - dragging across days extends it into a multi-day span (the start day fills from the start time down, whole middle days, the end day down to the end time), not a per-day rectangle. Releasing the drag only marks the range - it never creates an event on its own. Confirm it explicitly to create: press Enter, or right-click the selection and choose Add Event (Esc cancels the marker). Confirming fires onRangeSelect with the continuous start / end (plus the covered days[] / resourceIds[] for reference). Without a handler it falls back to onEventAdd(start, end, resourceId, allDay) - whose fourth argument is true for an all-day selection (the all-day row, a month day cell, or a multi-day timeline zoom), so the handler can set the allDayField and create a true all-day event.

You can also select by keyboard: click an empty cell to select it, then the arrow keys move the selected cell and Shift + arrows extend the range (Enter creates, Esc clears). What the arrows step by follows the view - in the time-grid a slot vertically and a day horizontally, in month a day horizontally and a week vertically, in the timeline one axis cell (slot / day / month) horizontally and a resource row vertically. In the time-grid the selection crosses into the all-day row: ArrowUp from the top timed cell moves to the all-day cell, and ArrowDown from the all-day row drops back into the grid.

Dragging the all-day row (or, in month, any day cells) selects whole days (selection.allDay is true); create an all-day event spanning start..end the same way.

Multi-selecting events (eventSelectable) works in every view too, including the agenda list: Ctrl/Cmd-click to toggle, Shift-click for a range, Delete to remove.

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end',
    rangeSelectable: true,
    onRangeSelect: (sel) => {
      // one continuous event over the whole marked range
      rows = [...rows, makeEvent(sel.start, sel.end, sel.allDay)]
    },
  }} />

Multi-select existing events with eventSelectable. Ctrl / Cmd-click toggles an event into the selection, Shift-click selects a range, and a plain click clears it. Selected events get an outline; dragging any one of them moves the whole set together, and pressing Delete removes them all (through onEventDelete). onEventSelectionChange reports the selected rows.

<SvGrid {data} {columns}
  scheduler={{
    startField: 'start', endField: 'end', editable: true,
    eventSelectable: true,
    onEventSelectionChange: (rows) => (selected = rows),
    onEventDelete: (row) => (data = data.filter((r) => r !== row)),
  }} />

Open the live example: Selecting cells & events (Scheduler)

Search, filter, sort flow through

Because the scheduler renders the grid's processed rows, the built-in search box (searchable, on by default) and any column filters or sort applied elsewhere all narrow the events shown - the calendar is never a separate data source.

Scheduler Pro

The Enterprise renderer adds a set of advanced capabilities on top of the base scheduler. They are read from the same scheduler prop; type your config as SchedulerProConfig (from @svgrid/enterprise) to get the extra fields typed.

Scheduler, not a Gantt. SvGrid's Scheduler is a resource / booking / calendar view of the grid - for appointments, staff and asset scheduling, and calendar apps. It is deliberately not a project planner: there is no critical path, percent-done, baselines, or work-breakdown structure, and none are planned. The dependencies below are an optional convenience for ordered bookings (a job that must move through stations in sequence); they are fully opt-in - with no dependency config the timeline draws no arrows and does no cascading.

Dependencies & auto-reschedule (optional, for ordered bookings)

Link events with predecessor -> successor dependencies and the timeline draws an arrow between them. When a predecessor is dragged or resized, its successors auto-reschedule forward just enough to keep every link legal - preserving each event's duration. Four link types are supported (FS finish-to-start, the default; SS, FF, SF) plus an optional lag in minutes (negative for a lead).

<script lang="ts">
  import { SvGrid } from '@svgrid/grid'
  import { enableSchedulerView, type SchedulerProConfig, type SchedulerDependency } from '@svgrid/enterprise'
  enableSchedulerView()

  const dependencies: SchedulerDependency[] = [
    { id: 'd1', from: 't1', to: 't2', type: 'FS' },      // t2 starts when t1 finishes
    { id: 'd2', from: 't1', to: 't3', type: 'FS', lag: 60 }, // + 1h lag
    { id: 'd3', from: 't2', to: 't4' },                  // FS (default)
  ]

  const cfg: SchedulerProConfig<any, Task> = {
    startField: 'start', endField: 'end',
    resourceField: 'team', resources: teams,
    initialView: 'timelineWeek', editable: true,
    businessHours: { start: 9, end: 17 },
    dependencies,
    autoReschedule: true,                 // default true when any dependency is set
    dependencyRespectWorkingTime: true,   // cascade skips outside business hours
    onDependenciesChange: (moves) => {    // persist the cascaded shifts
      for (const m of moves) {
        const row = rows.find((r) => r.id === m.id)
        if (row) { row.start = iso(m.start); row.end = iso(m.end) }
      }
    },
  }
</script>

from / to are row ids (your getRowId). Provide the links as a flat dependencies array, or per-row via dependencyField (an array of SchedulerDependency or successor-id strings on each row). A dependency that is not currently satisfied (a successor sitting too early) is drawn as a dashed red arrow; cyclic links are ignored so a cascade can never loop. Cascading only ever pushes successors forward - it never pulls an event earlier.

Arrows render in the timeline views (timelineDay / timelineWeek / timelineMonth / timelineYear), where events are laid out as horizontal bars.

Multi-assignment, histogram & summaries

By default one event sits on one resource (resourceField). With assignments a single event can be assigned to SEVERAL resources - it then renders under each of them in the timeline. Provide a flat assignments list, or a per-row assignmentField holding an array of resource ids.

const cfg: SchedulerProConfig<any, Shift> = {
  startField: 'start', endField: 'end',
  resourceField: 'assignees', resources: people,
  initialView: 'timelineWeek', editable: true,
  assignmentField: 'assignees',            // each row: resourceId[]
  resourceHistogram: { height: 22 },       // utilization bars under each row
  columnSummary: { label: 'Shifts', reducer: 'count' },  // sticky per-column totals
  onAssignmentChange: ({ eventId, from, to }) => {       // drag across rows to reassign
    const row = rows.find((r) => r.id === eventId)
    if (row && to) row.assignees = [...row.assignees.filter((a) => a !== from), to]
  },
}

Non-working-time collapse & zoom

Dense timelines waste width on hours nobody works. Turn on collapse and the axis folds non-working time out: nights (outside businessHours) and whole non-working days (nonWorkingDays, e.g. weekends) shrink to a thin gap, so the working hours fill the view. A zoom preset scales the tick size + pixel density from minutes out to months (a superset of slotSizes).

const cfg: SchedulerProConfig<any, Job> = {
  startField: 'start', endField: 'end',
  resourceField: 'machine', resources: machines,
  initialView: 'timelineWeek',
  businessHours: { start: 8, end: 18 }, nonWorkingDays: [0, 6],
  collapseNonWorking: true,   // fold nights to a gap
  collapseWeekends: true,     // fold weekends to a gap
  collapsedGapPx: 14,         // width of the gap marker (0 = omit entirely)
  zoom: 3,                    // ladder index: 0 = 5-min .. 7 = monthly
  onZoomChange: (level) => console.log(level.id),
}

The compressed axis is fully consistent - event bars, drag/resize snapping, the now-line and dependency arrows all map through it. A - <label> + stepper appears in the toolbar when zoom is set; supply your own ladder via zoomLevels. These apply to the timeline views only.

Grouped / tree resources

Organise the timeline's resource rows into a collapsible tree in the gutter - buildings to departments to providers/rooms. Define the group nodes with resourceGroups (nest via parentId) and map each resource to its group with resourceGroupOf. Click a group header to collapse its rows; the state persists via groupPersistKey.

const cfg: SchedulerProConfig<any, Appt> = {
  startField: 'start', endField: 'end',
  resourceField: 'provider', resources: providers, // flat resources
  resourceGroups: [
    { id: 'bldg-a', title: 'Main building' },
    { id: 'cardio', title: 'Cardiology', parentId: 'bldg-a' },
    { id: 'derm', title: 'Dermatology', parentId: 'bldg-a' },
  ],
  resourceGroupOf: (r) => r.dept,          // resource -> group id
  collapsibleGroups: true,
  groupPersistKey: 'clinic-groups',
  initialView: 'timelineDay',
}

Resources whose group is unknown / unset trail in an ungrouped section. Each group header shows a count of the leaf resources beneath it. (To group the timeline by an event field instead of a resource - e.g. by status or type - simply set resourceField to that field; the rows then bucket by its values.)

Skill-based eligibility

Restrict which events may be scheduled onto which resources (provider matching, room type, technician skills). Supply eligible(event, resource) - return false and a drag / resize / create onto that resource is rejected with a "Not eligible" flash, and ineligible rows hatch while you drag. Or use the declarative requiresField shortcut: an event's required tag(s) must all be in the resource's skills.

// Flexible predicate:
eligible: (job, tech) => tech.skills.includes(job.skill),

// or declarative - a `skills` array on each resource:
requiresField: 'skill',   // event field holding the required tag(s)
// resourceSkillsOf: (r) => r.skills,  // default reads r.skills

Utilization heatmap

Tint each resource row's background by how loaded it is per time-bucket - light when quiet, hot near capacity, red when over. Distinct from the histogram (a bar strip); the heatmap colours the whole lane so a glance shows the pressure.

resourceHistogram: false,
utilizationHeatmap: { capacityField: 'cap' },  // over `cap` = a red cell

Booking rules - buffers, lead time, duration, travel

Beyond conflicts and working hours, gate bookings on real-world rules. A move / resize / create that breaks one snaps back with the reason.

bufferBeforeMin: 15, bufferAfterMin: 15,   // clear gap around every booking
minLeadMin: 60,                             // no bookings within an hour of now
minDurationMin: 30, maxDurationMin: 90,
travelTimeOf: (a, b) => distanceMin(a.location, b.location), // widens the gap

Bookable slots / find-a-time

Turn the Day timeline into a booking surface: set bookable and every OPEN slot of durationMin lights up per resource - the free time left after existing bookings, each resource's own hours (availability), buffers and lead time. Clicking a slot books it.

bookable: { durationMin: 60, stepMin: 30 },
onSlotPick: (start, end, resourceId) => book(resourceId, start, end),

The slot math is also a pure export - availableSlots({ working, busy, durationMin, stepMin, bufferBeforeMin, bufferAfterMin, minStart }) from @svgrid/enterprise.

Multi-calendar overlay

Overlay several calendars, each colour-coded and toggleable from a legend. Hiding a calendar filters its events out of every view; an event's colour comes from its calendar unless colorField overrides.

calendars: [
  { id: 'work', title: 'Work', color: '#4f46e5' },
  { id: 'personal', title: 'Personal', color: '#16a34a' },
  { id: 'holidays', title: 'Holidays', color: '#d97706', hidden: true },
],
calendarField: 'cal',

Free/busy + find-a-time

Shade a resource's external busy time (from another calendar) with freeBusyOf, and find a slot that works for everyone with the pure commonFree(busyByPerson, dayStart, dayEnd, durationMin) export - the windows when all attendees are free.

freeBusyOf: (resource) => externalBusy[resource.id] ?? [],
// then, to suggest meeting times:
import { commonFree } from '@svgrid/enterprise'
const windows = commonFree(attendees.map(busyOf), day8, day18, 60)

Config reference

Option Purpose
startField (required) Field holding each event's start.
endField Field holding the end; else defaultDurationMin from the start.
allDayField Boolean field marking an all-day event; enables the drawer's All-day toggle.
titleField Event label. Defaults to the first column's field.
colorField / color Per-event accent color (the fill), or one for all.
secondaryColorField A second per-event color, painted as a left-edge strip (encode two dimensions - e.g. fill = person, strip = role).
recurrenceField Field holding a RecurrenceRule (or array).
recurrenceExceptionsField / onOccurrenceChange Per-occurrence overrides (moved / edited / deleted single instances); the prompt offers This event, This and following, and All events. onOccurrenceChange carries a scope - handle 'following' by splitting the series (see below); it also fires from a drag, delete, and a drawer save on a recurring occurrence.
defaultDurationMin Event length when a row has no endField (default 60).
views / initialView Which views to offer, and the first shown. Includes the horizontal timelineDay / timelineWeek / timelineMonth / timelineYear.
initialDate The date the calendar opens on (default today).
weekStartsOn First day of the week, 0-6 (default 0).
slotMinutes Time-grid slot size and move/resize snap (default 30). Each slot renders 30px tall, so the hour grows with the granularity (1h = 30px rows, 30m = 60px, 15m = 120px).
slotSizes Slot sizes (minutes) offered as a runtime ruler picker in Week / Day, e.g. [60, 30, 15, 5]. Shows a size selector in the toolbar; omit to hide it (the ruler still uses slotMinutes).
dayStartHour / dayEndHour Visible time-grid band (default 0..24).
nowIndicator Show the current-time line on today (Week / Day + Day timeline). On by default; false to hide.
businessHours / nonWorkingDays / shadeUntilNow Shade out-of-hours, non-working weekdays and elapsed time.
restrictToBusinessHours Enforce the shading - reject a drop / create outside working hours or on a non-working day.
disableConflicts Reject a drag / resize / create that double-books the same resource.
restrictedHours Hard-blocked hour bands (e.g. a lunch window [{ start: 12, end: 13 }]) - always non-bookable, rendered as a distinct red hatch.
restrictedDates Specific dates that are fully closed (no bookings).
specialDates Highlighted dates (holidays / launches) - a coloured strip + label on the column; decorative, does not block.
minDate / maxDate Earliest / latest navigable + bookable date (prev / next nav stop at the bounds).
maxEventsPerSlot Cap concurrent events per resource - reject a move / create that exceeds N overlapping events.
SchedulerResource.dateOverrides Per-date exceptions to a resource's weekly availability - a day off (off: true) or custom windows for one date.
statusField Free/busy status (busy / free / tentative / oof) driving an Outlook-style visual (solid / outline / hatched / tinted). Ties to iCal STATUS.
reminderField / onReminder Minutes-before-start for a reminder; fires onReminder (and a built-in toast) once as the lead time is crossed. Ties to iCal VALARM.
onUnschedule Drag an event onto the backlog panel to remove it from the schedule (typically push it back to unscheduled).
event / tooltip / tooltipDelay Custom event-body snippet and a hover-tooltip snippet (or tooltip: true for the built-in).
history Enable undo / redo of drag-move + resize (Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z).
unscheduled / onSchedule / backlogTitle A drag-to-schedule backlog panel beside the Week / Day grid.
timeZone IANA zone the calendar is shown in (ruler, positions, day boundaries, now-line). Default: browser local.
secondaryTimeZones Extra read-only hour rulers ({ id, label? }[]) shown left of the primary gutter - a world clock.
collisionMode Overlap layout: split (default) / cap / stack.
maxColumns cap mode: columns before a +N more tile (min 2, default 3).
agendaDays How many days the agenda spans (default 30).
resourceField / resources Group time-grid columns by resource + a legend/filter in every view; resources become the rows in timeline views.
groupByDate Group day->resources instead of resource->days (default false).
resourceAreaWidth Timeline: width (px) of the left resource-label gutter (default 160).
timelineSlotMinutes Timeline day zoom: tick size + move/resize snap (default slotMinutes).
timelineLaneHeight Timeline: height (px) of one event lane in a resource row (default 26).
editable Enable drag-to-move and edge-resize.
rangeSelectable / onRangeSelect Click / drag empty slots to mark a continuous date/time range (spans days; arrows move, Shift+arrows extend); confirm with Enter or right-click -> Add Event to create one event. On by default (false to disable).
eventSelectable / onEventSelectionChange Ctrl/Shift-click to multi-select events; drag to move together, Delete to remove. On by default (false to disable).
onEventMove / onEventResize Fired with the new start/end (and resource).
onEventAdd Fired when an empty slot is double-clicked.
onEventDelete Shows a Delete button in the drawer; remove the row here.
event A snippet for a custom event body.
drawer / onEventCommit Built-in detail drawer + save callback.
eventMenu Right-click menu items for an event.
searchable / searchPlaceholder The toolbar search box.

Scheduler Pro options (SchedulerProConfig)

Option Purpose
dependencies / dependencyField Predecessor -> successor links ({ id, from, to, type?, lag? }), as a flat list or per-row. Drawn as timeline arrows.
autoReschedule Cascade successors forward on move / resize to keep links legal (default true when dependencies are set).
dependencyRespectWorkingTime Cascade skips outside businessHours (a shifted successor lands at the next opening).
onDependenciesChange Fired with the cascaded { id, start, end } shifts - persist them here.
onDependencyAdd / onDependencyRemove User draws / removes a link.
assignments / assignmentField Many-to-many event <-> resource assignments; an event renders under every assigned resource.
resourceHistogram Per-resource utilization bars under each timeline row (true or { capacityField?, height? }); over-capacity bars turn red.
columnSummary Sticky per-time-column summary strip: { reducer: 'count' | { sum } | fn, label?, position? }.
onAssignmentChange Fired when an event is dragged from one resource row to another (reassign).
collapseNonWorking / collapseWeekends Fold nights / non-working days out of the timeline axis.
collapsedGapPx Width (px) of a collapsed-gap marker (default 12; 0 omits it).
zoom / zoomLevels / onZoomChange Continuous zoom preset (minutes -> months) with a toolbar stepper; supersedes slotSizes.
resourceGroups / resourceGroupOf Group timeline resource rows into a collapsible tree ({ id, title, parentId? }[] + a resource -> group-id map).
collapsibleGroups / groupPersistKey Allow collapsing groups (default on) and persist the collapsed set to localStorage.
eligible / requiresField / resourceSkillsOf Skill / eligibility rule - which events may drop on which resources; rejected drops flash "Not eligible".
utilizationHeatmap Tint each resource row's background by load per bucket (true or { capacityField }); over capacity = red.
bufferBeforeMin / bufferAfterMin Required clear minutes around each booking (number or (event) => number).
minLeadMin / minDurationMin / maxDurationMin Minimum notice, and min / max booking duration in minutes.
travelTimeOf Travel minutes required between two consecutive bookings on a resource (widens the buffer).
bookable / onSlotPick Show open bookable slots of a duration on the Day timeline ({ durationMin, stepMin? }); click to book.
calendars / calendarField Overlay several colour-coded calendars with a toggleable legend; map events via calendarField.
freeBusyOf External busy intervals to shade on a resource row (for find-a-time). Pair with the commonFree export.

More examples

Scheduling rules & policies

Every booking policy the scheduler can enforce: business hours + a hard-blocked lunch band, a closed date, a highlighted holiday, min/max navigable dates, room capacity, a per-doctor day off, free/busy statuses, per-event reminders, drag-to-unschedule, and This / This-and-following / All recurrence edits.

Open the live example: Scheduling rules & policies (Scheduler)

Sequenced bookings

A service centre where a job flows through stations in order - a booking can only start once the one it depends on finishes. Drag or resize a booking and the steps that follow slide forward (opening-hours aware). Dependencies are an optional convenience for ordered bookings. Toggle to the Table - same grid rows, just a view.

Open the live example: Sequenced bookings (Scheduler)

Staffing board - assignments & utilization

A weekly staffing timeline where one shift can cover several people (it appears under each). A per-person utilization histogram sits under each row (red where over-booked) and a sticky summary strip totals shifts per day. Drag a shift onto another person to reassign.

Open the live example: Staffing board - assignments & utilization (Scheduler)

Operations timeline - collapse & zoom

A shop-floor timeline that collapses non-working time: nights fold to a thin gap and weekends to a marker, so working hours fill the width. A zoom stepper scales the axis from hourly detail out to weekly; bars, drag and the now-line all map through the compressed axis.

Open the live example: Operations timeline - collapse & zoom (Scheduler)

Grouped resources

A clinic day where providers are organised into a collapsible tree in the gutter - buildings to departments to providers/rooms. Click a group to collapse it (persisted); appointments schedule onto the leaf providers. Toggle to the Table - the groups just read the same grid rows.

Open the live example: Grouped resources (Scheduler)

Utilization heatmap

A support centre where each queue has a capacity and calls overlap through the day. The row background is tinted by how loaded the queue is each hour - light when quiet, hot near capacity, red when over. A glance shows where the pressure is.

Open the live example: Utilization heatmap (Scheduler)

Multi-calendar overlay

Several calendars overlaid on one week - Work, Personal, Family, Holidays - each colour-coded from a legend you can toggle. Turning a calendar off hides its events across every view; an event takes its calendar colour.

Open the live example: Multi-calendar overlay (Scheduler)

Live 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.
  • 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.
  • 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.
  • Portfolio roadmap - initiatives across squads - A dashboard-forward portfolio planner: a live portfolio-health strip (status mix, capacity by squad, delivery trend, on-track gauge) over a Gantt of squads x initiatives with progress bars and milestones.
  • 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.
  • Broadcast - content & marketing calendar - A content command center: one grid, two views - a Calendar / Board toggle over the same posts. The Board is the approval pipeline (Idea / Draft / In review / Scheduled / Published) with per-card engagement sparklines.
  • 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.
  • Scheduling rules & policies - Every booking policy the scheduler can enforce: business hours + a hard-blocked lunch band, a closed date, a highlighted holiday, min/max navigable dates, room capacity, a per-doctor day off, free/busy statuses, per-event reminders, drag-to-unschedule, and This / This-and-following / All recurrence edits.
  • Sequenced bookings - A service centre where a job flows through stations in order - a booking can only start once the one it depends on finishes. Drag or resize a booking and the steps that follow slide forward (opening-hours aware). Dependencies are an optional convenience for ordered bookings. Toggle to the Table - same grid rows, just a view.
  • Staffing board - assignments & utilization - A weekly staffing timeline where one shift can cover several people (it appears under each). A per-person utilization histogram sits under each row (red where over-booked) and a sticky summary strip totals shifts per day. Drag a shift onto another person to reassign.
  • Operations timeline - collapse & zoom - A shop-floor timeline that collapses non-working time: nights fold to a thin gap and weekends to a marker, so working hours fill the width. A zoom stepper scales the axis from hourly detail out to weekly; bars, drag and the now-line all map through the compressed axis.
  • Grouped resources - A clinic day where providers are organised into a collapsible tree in the gutter - buildings to departments to providers/rooms. Click a group to collapse it (persisted); appointments schedule onto the leaf providers. Toggle to the Table - the groups just read the same grid rows.
  • Utilization heatmap - A support centre where each queue has a capacity and calls overlap through the day. The row background is tinted by how loaded the queue is each hour - light when quiet, hot near capacity, red when over. A glance shows where the pressure is.
  • Multi-calendar overlay - Several calendars overlaid on one week - Work, Personal, Family, Holidays - each colour-coded from a legend you can toggle. Turning a calendar off hides its events across every view; an event takes its calendar colour.