Customization

Custom Rendering

Every visible part of the scheduler that is not the grid itself can be drawn by you. You pass a Svelte snippet; the scheduler calls it with typed arguments and keeps the behaviour, so a custom event still selects, drags and opens.

Snippets, Not Render Functions

Ten props take a snippet. Four of them are on this page; the others have pages of their own. All argument types are exported, so a snippet declared outside the component can be typed.

The ten snippet props

PropReceivesWhat it does
eventEventSnippetPropsReplaces the chip, everywhere
cellCellSnippetPropsAdds a layer under a column or month cell
headerHeaderSnippetPropsReplaces the week column header and the day title
emptyEmptySnippetPropsReplaces the empty state of week, day and agenda
eventDetailEventDetailSnippetPropsAdds to the popover and slide-over. Creating & Editing
eventPanelEventPanelSnippetPropsReplaces the slide-over body. Creating & Editing
createPanelCreatePanelSnippetPropsThe panel a drag or Enter opens. Creating & Editing
toolbarToolbarSnippetPropsReplaces the toolbar. Toolbar
toolbarActionsnothingAdds buttons to the built-in toolbar. Toolbar
sidebar, sidebarHeader, sidebarFooterSidebarSnippetPropsReplace or extend the sidebar. Sidebar

Events

The event snippet replaces the chip. It receives the event, its position on the grid, the view name and three flags. The cards below change their border with each flag: select one, drag one, resize one. A taller slotHeight gives a card room for a second line.

Oct 19 – 25, 2026

GMT+7
Mon 19
Tue 20
Wed 21
Thu 22
Fri 23
Sat 24
Sun 25
<script lang="ts">
  import { Icon } from 'sv5ui';
  import { Scheduler, type EventInput, type EventSnippetProps } from '@sv5ui/scheduler';

  interface Meeting { location: string; attendees: string[]; icon: string }
  let events = $state<EventInput<Meeting>[]>([/* ... */]);
</script>

<!-- Replaces the chip everywhere an event is drawn. slotHeight gives the card room. -->
<Scheduler bind:events event={card} slotHeight={36} />

{#snippet card({ event, position, isSelected, isDragging, isResizing }: EventSnippetProps<Meeting>)}
  <button
    type="button"
    class={[
      'flex h-full w-full flex-col gap-1 overflow-hidden rounded-lg border-s-4 border-primary bg-primary-container/80 p-2 text-start text-on-primary-container',
      isSelected && 'ring-2 ring-primary',
      isDragging && 'opacity-60',
      isResizing && 'ring-2 ring-warning'
    ]}
  >
    <span class="flex items-center gap-1.5 text-xs font-semibold">
      <Icon name={event.data?.icon ?? 'lucide:calendar'} size="14" />
      <span class="truncate">{event.title}</span>
    </span>
    {#if position?.kind !== 'time' || position.height > 48}
      <span class="truncate text-[11px] opacity-80">{event.data?.location}</span>
    {/if}
  </button>
{/snippet}
// What the event snippet receives
interface EventSnippetProps<T> {
  event: SchedulerEvent<T>;          // start and end are the segment's, for an event that crosses midnight
  position?: PositionedEvent<T>;     // undefined in the agenda list
  view: string;                      // 'week', 'month', a custom name
  isSelected: boolean;
  isDragging: boolean;               // this event is being moved
  isResizing: boolean;               // this event is being resized
}

// position.kind tells you which shape you are drawing
position.kind === 'time'   // a block in the time grid: top, height in px; left, width as fractions of the column
position.kind === 'span'   // a bar in the all-day row or the month: startColumn, endColumn, lane, continuesBefore, continuesAfter

// Hide detail when the block is short
{#if position?.kind === 'time' && position.height > 48} ... {/if}

Where the Event Snippet Applies

Everywhere an event is drawn: a block in the time grid, a bar in the all-day row, a bar in a month cell, a line in the list behind +N, a row in the agenda. The year view draws no events. Switch the demo above to Month and Agenda to see the same snippet in each. The wrapper around your markup takes the click, so selection, onEventClick and the detail popover work as before. It is not focusable, though, so render a button as the demo does; otherwise Tab skips your events and a screen reader never hears them.

<!-- The wrapper the scheduler puts around your snippet is a div with role="presentation".
     It takes the click, so selection, onEventClick and the detail popover still work,
     but it is not focusable. Render a button so the keyboard and a screen reader reach
     the event; aria-pressed and aria-haspopup are yours to add if you want them. -->
{#snippet card({ event, isSelected }: EventSnippetProps)}
  <button type="button" aria-pressed={isSelected} class="h-full w-full text-start ...">
    {event.title}
  </button>
{/snippet}

Reusing EventChip Inside a Snippet

To keep the chip's look and change only its content, render EventChip yourself with children. A chip rendered by you does not read the calendar colour on its own, so look it up and pass color.

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">
  import { EventChip, Scheduler, type EventSnippetProps } from '@sv5ui/scheduler';
</script>

<!-- Keep the chip, change what is inside it. A chip outside the grid does not
     read the calendar colour on its own, so pass color. -->
<Scheduler bind:events {calendars} event={chip} />

{#snippet chip({ event, position, isSelected, isDragging }: EventSnippetProps<Meeting>)}
  <EventChip
    {event}
    {position}
    color={calendars.find((c) => c.id === event.calendarId)?.color ?? event.color}
    selected={isSelected}
    dragging={isDragging}
    class="h-full w-full"
  >
    <Icon name={event.data?.icon ?? 'lucide:calendar'} size="12" />
    <span class="truncate">{event.title}</span>
  </EventChip>
{/snippet}

Cells

The cell snippet is a layer under the events of a month cell or a time grid column. It is rendered after the off-hours shading and before the chips, so use absolute inset-0 pointer-events-none and let the grid keep the pointer. The demo stripes weekends and holidays and tints the anchor day.

October 2026

Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 Founders day
21
22
23
24
25
26
27
28
29
30 Company holiday
31
1
<!-- A layer under the events of a month cell or a time grid column. It sits after
     the off-hours shading and before the chips, so it must not take the pointer. -->
<Scheduler bind:events cell={backdrop} />

{#snippet backdrop({ date, isWeekend, isAnchor, isHoliday, isToday }: CellSnippetProps)}
  {#if isWeekend || isHoliday}
    <div class="pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(135deg,transparent_0_8px,var(--color-outline-variant)_8px_9px)] opacity-60"></div>
  {/if}
  {#if isAnchor && !isToday}
    <div class="pointer-events-none absolute inset-0 bg-tertiary/10"></div>
  {/if}
{/snippet}
interface CellSnippetProps {
  date: ZonedDateTime;      // midnight of the cell's day
  view: string;
  isToday: boolean;
  isAnchor: boolean;        // the day the date prop points at
  isWeekend: boolean;       // Saturday or Sunday
  isHoliday: boolean;       // in holidays
  isBusinessHours: boolean; // a working day: in businessHours.days and not a holiday; a weekday when there are no businessHours
  isOutside: boolean;       // a month cell that belongs to the previous or next month
}

Column Headers

header replaces the column header of the week view and the title of the day view. It receives a ready-made label in the current locale and the holiday, if any. The day view keeps its own line under your markup with the week number and the zone label. The month and year views have no column headers to replace.

Oct 19 – 25, 2026

GMT+7
Mon 19
Tue 20 Today
Wed 21
Thu 22
Fri 23
Sat 24
Sun 25
All day
Lunch break
<!-- The column header of the week view and the title of the day view.
     The month and year views have no column headers to replace. -->
<Scheduler bind:events {holidays} header={dayHeader} />

{#snippet dayHeader({ label, date, isToday, holiday }: HeaderSnippetProps)}
  <div class="flex flex-col items-center gap-1 py-0.5">
    <span class="text-xs font-semibold">{label}</span>   <!-- 'Mon 14' in the week, 'Monday, September 14, 2026' in the day -->
    {#if isToday}
      <Badge label="Today" color="primary" variant="solid" size="xs" />
    {:else if holiday}
      <Badge label={holiday.title ?? 'Holiday'} color="tertiary" variant="soft" size="xs" />
    {/if}
  </div>
{/snippet}

<!-- The day view keeps its own line under the snippet: week number and zone label. -->

Empty State

empty is shown by the week, day and agenda views when their range has no events. The month and year grids never show it: their empty cells already say so. It is held back while a source is loading, so it does not flash before the data arrives.

Oct 19 – 25, 2026

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

A quiet week

Nothing is planned for this week. Drag across the grid to add something.

<script lang="ts">
  import { Empty } from 'sv5ui';
</script>

<!-- Shown by the week, day and agenda views when the range has no events.
     The month and year grids never show it: their empty cells already say so.
     It is held back while a source is loading, so it never flashes before data. -->
<Scheduler bind:events empty={quiet} />

{#snippet quiet({ view, range }: EmptySnippetProps)}
  <Empty
    icon="lucide:coffee"
    title="A quiet {view === 'agenda' ? 'month' : view}"
    description="Nothing is planned. Drag across the grid to add something."
  />
{/snippet}

Details

The popover and the slide-over have their own snippets, eventDetail and eventPanel, on Creating & Editing.

Typing Snippets

Type the array as EventInput<Meeting>[] and annotate the snippet argument with the exported type. TypeScript then checks event.data inside the snippet.

<script lang="ts">
  import type { EventInput, EventSnippetProps, CellSnippetProps } from '@sv5ui/scheduler';

  interface Meeting { location: string }

  // The array's T flows into every snippet: event.data is Meeting | undefined
  let events = $state<EventInput<Meeting>[]>([]);
</script>

<Scheduler bind:events event={card} cell={backdrop} />

{#snippet card({ event }: EventSnippetProps<Meeting>)}
  {event.data?.location}
{/snippet}

{#snippet backdrop({ isWeekend }: CellSnippetProps)}
  ...
{/snippet}

Props

The snippet props on this page, and the two settings the demos lean on.

PropTypeDefault
eventSnippet<[EventSnippetProps<T>]>-
cellSnippet<[CellSnippetProps]>-
headerSnippet<[HeaderSnippetProps]>-
emptySnippet<[EmptySnippetProps]>-
slotHeightnumber24
holidaysHoliday[][]

Snippets

What the four snippets receive. Every type is exported from @sv5ui/scheduler.

TypeFields
EventSnippetPropsevent, position, view, isDragging, isResizing, isSelected
CellSnippetPropsdate, view, isToday, isAnchor, isWeekend, isHoliday, isBusinessHours, isOutside
HeaderSnippetPropsdate, view, label, isToday, isAnchor, holiday
EmptySnippetPropsview, range