Customization

Sidebar

A panel beside the grid, or over it on a narrow screen. Turn on the built-in one with a single prop, add to it, or replace it with your own.

The Built-in Sidebar

sidebar alone turns it on. It shows four parts, two of them only when the matching prop is given, and takes a header and a footer. The demo docks at 768px because the docs column is narrower than the default breakpoint; see the next section.

Parts

PartWhat it does
DateNavigatorA month calendar. A dot under every day that has events after the filters; picking a day navigates to it
SearchBoxBound to search. The / key focuses it from anywhere on the page
CalendarListOne checkbox per calendar, bound to hiddenCalendars. Only when calendars is given
DragSourceListThe backlog of dragSources, ready to drop on the grid. Only when dragSources is given

Oct 19 – 25, 2026

GMT+7
Mon 19
Tue 20
Wed 21
Thu 22
Fri 23
Sat 24
Sun 25
All day
Lunch break
<!-- sidebar alone turns on the built-in one: a date navigator, a search box,
     the calendar list when there are calendars, the backlog when there are dragSources -->
<Scheduler
  bind:events
  bind:sidebarOpen
  sidebar
  sidebarSide="start"
  {calendars}
  {dragSources}
>
  {#snippet sidebarHeader()}
    <p class="text-sm font-semibold">Team calendar</p>
  {/snippet}
  {#snippet sidebarFooter()}
    <p class="text-xs text-on-surface/60">Synced a moment ago</p>
  {/snippet}
</Scheduler>

Docked or Slide-over

The sidebar docks beside the grid when the scheduler is at least sidebarBreakpoint pixels wide, 1024 by default, and becomes a slide-over inside the calendar below that. The menu button in the toolbar opens the slide-over; picking a date in it closes it. sidebarOpen controls only the docked state. The toggle below moves the breakpoint above and below the demo's width, so you can see both without resizing the window; before the scheduler has measured itself, on the server, it counts as docked.

Oct 19 – 25, 2026

GMT+7
Mon 19
Tue 20
Wed 21
Thu 22
Fri 23
Sat 24
Sun 25
All day
Lunch break
<!-- The sidebar docks beside the grid when the scheduler is at least
     sidebarBreakpoint pixels wide (1024 by default), and becomes a slide-over
     inside the calendar below that. Picking a date in the slide-over closes it.
     sidebarOpen only controls the docked state. -->
<Scheduler bind:events sidebar sidebarBreakpoint={768} />

<!-- Always a slide-over, whatever the width -->
<Scheduler bind:events sidebar sidebarBreakpoint={Infinity} />

Side

sidebarSide is start or end, relative to the reading direction: start is the left in LTR and the right under dir="rtl". The slide-over comes in from the same side.

<!-- start is the left in LTR and the right in RTL; end is the other side -->
<Scheduler bind:events sidebar sidebarSide="end" />

Adding Content

sidebarHeader and sidebarFooter render above and below the built-in parts and receive the same arguments a custom sidebar does. The demo's header opens the create panel for 9:00 on the anchor day through draft; the footer counts what is left after the filters.

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 { now } from '@internationalized/date';
  import { Scheduler, type SidebarSnippetProps, type SlotSelection } from '@sv5ui/scheduler';

  let draft = $state<SlotSelection | null>(null);
</script>

<!-- Both receive SidebarSnippetProps. The header here opens the create panel
     through draft; the footer reads the filtered store. -->
<Scheduler bind:events bind:draft sidebar createPanel={form}>
  {#snippet sidebarHeader({ date }: SidebarSnippetProps)}
    <Button
      label="New event"
      icon="lucide:plus"
      block
      onclick={() => {
        const start = date.set({ hour: 9, minute: 0 });
        draft = { start, end: start.add({ hours: 1 }), allDay: false };
      }}
    />
  {/snippet}
  {#snippet sidebarFooter({ events }: SidebarSnippetProps)}
    <p class="text-xs text-on-surface/60">{events.length} events after filters</p>
  {/snippet}
</Scheduler>

A Sidebar of Your Own

Pass a snippet as sidebar and it replaces the whole panel, docked and slide-over alike. It receives the anchor date, the range, the filtered store and a navigate function. docked tells you whether to show a close button, and events lists series as one entry each, not expanded, so the demo leaves them out of "Up next".

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 snippet as sidebar replaces the whole panel. events is the filtered store
     with series not expanded, so filter recurrence out when listing what is next. -->
<Scheduler bind:events sidebar={upNext} />

{#snippet upNext({ date, events, navigate, docked, close }: SidebarSnippetProps)}
  <div class="flex h-full flex-col gap-4 p-4">
    <div class="flex items-center justify-between">
      <h3 class="font-semibold">Up next</h3>
      {#if !docked}
        <Button icon="lucide:x" variant="ghost" size="xs" aria-label="Close" onclick={close} />
      {/if}
    </div>
    <ul class="space-y-2">
      {#each events
        .filter((e) => !e.recurrence && !e.background && e.end.compare(date) > 0)
        .toSorted((a, b) => a.start.compare(b.start))
        .slice(0, 6) as event (event.id)}
        <li>
          <button type="button" class="w-full rounded-xl border p-3 text-start" onclick={() => navigate(event.start, 'day')}>
            <p class="text-sm font-medium">{event.title}</p>
            <p class="text-xs text-on-surface/60">{describeTime(event)}</p>
          </button>
        </li>
      {/each}
    </ul>
  </div>
{/snippet}
interface SidebarSnippetProps<T> {
  date: ZonedDateTime;                   // the anchor
  view: string;
  range: DateRange;
  events: SchedulerEvent<T>[];           // the whole store after hiddenCalendars, search and filter; series as one entry
  scheduler: SchedulerContext;
  navigate: (date: ZonedDateTime, view?: string) => void;
  close: () => void;                     // closes the slide-over, or the docked panel (sidebarOpen becomes false)
  docked: boolean;                       // true beside the grid, false as a slide-over
}

Filter by Your Own Data

Put the controls in the header and the predicate in filter. The built-in parts follow: the navigator's dots and the search results only count events that pass. See Calendars & Filtering for the three filters and their order.

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">
  interface Meeting { status: 'confirmed' | 'tentative'; mine: boolean }
  let status = $state('all');
  let onlyMine = $state(false);
</script>

<!-- Controls in the sidebar header, the predicate on the scheduler. The built-in
     parts keep working: the date navigator's dots follow the filter too. -->
<Scheduler
  bind:events
  {calendars}
  sidebar
  filter={(event) =>
    (status === 'all' || event.data?.status === status) && (!onlyMine || event.data?.mine === true)}
>
  {#snippet sidebarHeader()}
    <ToggleGroup items={statusItems} bind:value={status} size="sm" />
    <Switch label="Only mine" bind:checked={onlyMine} />
  {/snippet}
</Scheduler>

Outside the Scheduler

The four parts are exported on their own. For a layout of your own, a sidebar shared by two calendars, or a search box in the page header, render them where you like and bind them to the same state; see Building Blocks.

Props

The sidebar props. sidebarOpen is bindable.

PropTypeDefault
sidebarboolean | Snippet<[SidebarSnippetProps<T>]>false
sidebarHeaderSnippet<[SidebarSnippetProps<T>]>-
sidebarFooterSnippet<[SidebarSnippetProps<T>]>-
sidebarOpenbooleantrue
sidebarSide'start' | 'end''start'
sidebarBreakpointnumber1024
dragSourcesDragSourceData<T>[][]
filter(event: SchedulerEvent<T>) => boolean-

Snippets

What sidebar, sidebarHeader and sidebarFooter receive.

TypeFields
SidebarSnippetPropsdate, view, range, events, scheduler, navigate, close, docked