Vize

Scheduler

Headless, SSR-deterministic day/week/month scheduler with a pure event layout engine.

Package @vizejs/ui/scheduler
Maturity stable
Own the source vize lib pull scheduler
Requires calendar, context, controllable-state, datetime-field, drag-and-drop, id, locale
Aliases calendar view, event calendar, agenda, week planner, big calendar
Covers FullCalendar views, React Big Calendar, WAI-ARIA grid, Google Calendar layout

Minimal setup

Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).

vp install @vizejs/ui

Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.

import { Scheduler, SchedulerRoot, SchedulerDayColumn, SchedulerEvent, SchedulerHeading, SchedulerMonthDay, SchedulerMonthGrid, SchedulerNav, SchedulerTimeGrid } from "@vizejs/ui/scheduler";

Or copy the source into your project with vize lib pull scheduler (see Source Distribution).

Try it

This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.

Open the interactive example · Styling these components

Copy the example

<!-- Week scheduler with typed events that applies drag and keyboard moves and resizes. -->
<script setup lang="ts">
import { ref } from "vue";

import { createPlainDateTime, SchedulerRoot } from "@vizejs/ui/scheduler";
import type { SchedulerEventChange, SchedulerEventData } from "@vizejs/ui/scheduler";

interface Booking {
  readonly room: string;
}

const events = ref<readonly SchedulerEventData<Booking>[]>([
  {
    id: "review",
    title: "Design review",
    data: { room: "Aurora" },
    start: createPlainDateTime(2026, 10, 22, 9, 0),
    end: createPlainDateTime(2026, 10, 22, 10, 30),
  },
  {
    id: "standup",
    title: "Standup",
    data: { room: "Cedar" },
    start: createPlainDateTime(2026, 10, 22, 9, 30),
    end: createPlainDateTime(2026, 10, 22, 9, 45),
  },
]);

function apply(change: SchedulerEventChange<Booking>): void {
  events.value = events.value.map((event) =>
    event.id === change.event.id ? { ...event, start: change.start, end: change.end } : event,
  );
}
</script>

<template>
  <SchedulerRoot
    :events
    aria-label="Room bookings"
    locale="en-US"
    :today="{ year: 2026, month: 10, day: 22 }"
    :day-start-hour="8"
    :day-end-hour="12"
    @event-move="apply"
    @event-resize="apply"
  />
</template>

API

Scheduler<Data>

Source: src/families/date-time/scheduler/scheduler-root.vue

Props

Prop Type Default Description
id string | null | undefined undefined Consumer-owned base id; nullish values use a deterministic fallback.
events readonly SchedulerEvent<Data>[] | undefined [] Events to lay out; data keeps the consumer payload type.
view SchedulerView | undefined undefined Controlled view; undefined selects uncontrolled mode.
defaultView SchedulerView | undefined "week" Initial uncontrolled view.
date PlainDate | null | undefined undefined Controlled anchor date deciding the visible period.
defaultDate PlainDate | null | undefined undefined Initial uncontrolled anchor date.
today PlainDate | null | undefined undefined Explicit current date; the SSR-safe way to anchor and mark today.
now DateTimeNow | undefined undefined Injectable clock evaluated during setup on server and client.
timeZone string | undefined undefined IANA time zone used with now and the post-mount host clock.
locale string | undefined undefined BCP 47 locale; defaults to the nearest LocaleProvider.
dir "ltr" | "rtl" | undefined undefined Text direction; defaults to the nearest LocaleProvider.
weekStartsOn Weekday | undefined undefined First day of week; defaults to the locale preference.
dayStartHour number | undefined 0 First visible hour of the time grid.
dayEndHour number | undefined 24 Hour where the time grid ends, exclusive.
slotMinutes number | undefined 30 Minutes per time-grid slot row.
snapMinutes number | undefined slotMinutes Minutes that drag and keyboard moves snap to.
minimumEventMinutes number | undefined 15 Minimum rendered event length in minutes.
maxLanes number | undefined 3 Month-view lanes rendered per week before counting overflow.
disabled boolean | undefined false Disable navigation, activation, and editing.
readOnly boolean | undefined false Keep navigation and activation but block move and resize requests.
ariaLabel string | undefined undefined Accessible name for the scheduler region.

Events

Event Payload Description
update:view [view: SchedulerView] Fired when the scheduler requests a new controlled view.
update:date [date: PlainDate] Fired when navigation requests a new anchor date.
event-activate [event: SchedulerEvent<Data>, nativeEvent: Event] Fired when an event is clicked or activated with Enter/Space.
slot-activate [range: SchedulerSlotRange, nativeEvent: Event] Fired when a time-grid slot is clicked or activated.
day-activate [date: PlainDate, nativeEvent: Event] Fired when a month-view day is clicked or activated.
event-move [change: SchedulerEventChange<Data>, nativeEvent: Event | null] Fired when a drag or Alt+Arrow requests a new start (duration preserved).
event-resize [change: SchedulerEventChange<Data>, nativeEvent: Event | null] Fired when a resize drag or Alt+Shift+Arrow requests a new end.

Slots

Slot Slot props Description
default SchedulerSlotState<Data> Scheduler composition. Receives typed layout state; defaults to a header plus the view's grid.

SchedulerRoot<Data>

Source: src/families/date-time/scheduler/scheduler-root.vue

Props

Prop Type Default Description
id string | null | undefined undefined Consumer-owned base id; nullish values use a deterministic fallback.
events readonly SchedulerEvent<Data>[] | undefined [] Events to lay out; data keeps the consumer payload type.
view SchedulerView | undefined undefined Controlled view; undefined selects uncontrolled mode.
defaultView SchedulerView | undefined "week" Initial uncontrolled view.
date PlainDate | null | undefined undefined Controlled anchor date deciding the visible period.
defaultDate PlainDate | null | undefined undefined Initial uncontrolled anchor date.
today PlainDate | null | undefined undefined Explicit current date; the SSR-safe way to anchor and mark today.
now DateTimeNow | undefined undefined Injectable clock evaluated during setup on server and client.
timeZone string | undefined undefined IANA time zone used with now and the post-mount host clock.
locale string | undefined undefined BCP 47 locale; defaults to the nearest LocaleProvider.
dir "ltr" | "rtl" | undefined undefined Text direction; defaults to the nearest LocaleProvider.
weekStartsOn Weekday | undefined undefined First day of week; defaults to the locale preference.
dayStartHour number | undefined 0 First visible hour of the time grid.
dayEndHour number | undefined 24 Hour where the time grid ends, exclusive.
slotMinutes number | undefined 30 Minutes per time-grid slot row.
snapMinutes number | undefined slotMinutes Minutes that drag and keyboard moves snap to.
minimumEventMinutes number | undefined 15 Minimum rendered event length in minutes.
maxLanes number | undefined 3 Month-view lanes rendered per week before counting overflow.
disabled boolean | undefined false Disable navigation, activation, and editing.
readOnly boolean | undefined false Keep navigation and activation but block move and resize requests.
ariaLabel string | undefined undefined Accessible name for the scheduler region.

Events

Event Payload Description
update:view [view: SchedulerView] Fired when the scheduler requests a new controlled view.
update:date [date: PlainDate] Fired when navigation requests a new anchor date.
event-activate [event: SchedulerEvent<Data>, nativeEvent: Event] Fired when an event is clicked or activated with Enter/Space.
slot-activate [range: SchedulerSlotRange, nativeEvent: Event] Fired when a time-grid slot is clicked or activated.
day-activate [date: PlainDate, nativeEvent: Event] Fired when a month-view day is clicked or activated.
event-move [change: SchedulerEventChange<Data>, nativeEvent: Event | null] Fired when a drag or Alt+Arrow requests a new start (duration preserved).
event-resize [change: SchedulerEventChange<Data>, nativeEvent: Event | null] Fired when a resize drag or Alt+Shift+Arrow requests a new end.

Slots

Slot Slot props Description
default SchedulerSlotState<Data> Scheduler composition. Receives typed layout state; defaults to a header plus the view's grid.

SchedulerDayColumn

Source: src/families/date-time/scheduler/scheduler-day-column.vue

Props

Prop Type Default Description
column (required) SchedulerDayColumnState<unknown> undefined (required) Day and timed placements rendered by this column.

Slots

Slot Slot props Description
event TimeGridPlacement<unknown> Event content for each placement; defaults to the event title.

SchedulerEvent

Source: src/families/date-time/scheduler/scheduler-event.vue

Props

Prop Type Default Description
event (required) SchedulerEvent<unknown> undefined (required) Event rendered by this item.
placement TimeGridPlacement<unknown> | undefined undefined Time-grid placement; drives --vize-scheduler-* position variables.
row RowPlacement<unknown> | undefined undefined Row placement for all-day lanes and month weeks.
resizable boolean true Render a resize handle at the end of time-grid events.

Slots

Slot Slot props Description
default { readonly event: SchedulerEvent<unknown>; readonly placement: TimeGridPlacement<unknown> | undefined; readonly row: RowPlacement<unknown> | undefined; readonly label: string; } Event content. Receives the event and its placement; defaults to the title.

SchedulerHeading

Source: src/families/date-time/scheduler/scheduler-heading.vue

Slots

Slot Slot props Description
default { readonly heading: string } Heading content. Receives the localized period label.

SchedulerMonthDay

Source: src/families/date-time/scheduler/scheduler-month-day.vue

Props

Prop Type Default Description
week (required) SchedulerWeekState<unknown> undefined (required) Week row that owns this day.
index (required) number undefined (required) Day index inside the week.

Slots

Slot Slot props Description
day SchedulerDay Day number content.
event RowPlacement<unknown> Event bar content for events starting in this cell.
more { readonly count: number; readonly day: SchedulerDay } Overflow indicator content when more events exist than maxLanes.

SchedulerMonthGrid

Source: src/families/date-time/scheduler/scheduler-month-grid.vue

Slots

Slot Slot props Description
weekday SchedulerDay Weekday column header content.
day SchedulerDay Day number content.
event RowPlacement<unknown> Event bar content.
more { readonly count: number; readonly day: SchedulerDay } Overflow indicator content.

SchedulerNav

Source: src/families/date-time/scheduler/scheduler-nav.vue

Props

Prop Type Default Description
action (required) "previous" | "next" | "today" undefined (required) Navigation performed by the button.
ariaLabel string undefined Accessible name; defaults to "Previous period", "Next period", or "Today".

Slots

Slot Slot props Description
default { readonly action: "previous" | "next" | "today" } Button content. Receives the action.

SchedulerTimeGrid

Source: src/families/date-time/scheduler/scheduler-time-grid.vue

Props

Prop Type Default Description
timeHeader string "Time" Corner column header announced for the time labels.
allDayLabel string "All day" Accessible name of the all-day lane group.

Slots

Slot Slot props Description
dayHeader SchedulerDay Day column header content.
time SchedulerSlotRow Time label content for each slot row.
event TimeGridPlacement<unknown> Timed event content.
allDayEvent RowPlacement<unknown> All-day event content.

Behavior

Normative state x input -> outcome table for the headless Scheduler compound (@vizejs/ui/scheduler): scheduler-root.vue (generic over the event payload Data), scheduler-heading.vue, scheduler-nav.vue, scheduler-time-grid.vue, scheduler-day-column.vue, scheduler-month-grid.vue, scheduler-month-day.vue, and scheduler-event.vue, plus the pure layout engine scheduler-layout.ts. Every row is proven by the named test.

# State Input Outcome Proven by
S1 layout overlapping timed events events sort by start (longer first), group into overlap clusters, take the first free column, and widen across free columns; top/height are fractions of the visible window overlapping events split into columns and widen into free space, three-way overlaps widen short events into free columns
S2 layout window / short / overnight / all-day placements clip to dayStartHour–dayEndHour with continuation flags, get a minimum rendered length, and skip all-day events the day window clips events, enforces a minimum length, and skips all-day events
S3 layout multi-day and all-day events row layout stacks events into lanes (longer first) with continuation flags across the row row layout stacks multi-day events into lanes with continuation flags
S4 layout views / anchors / pointer visibleDays, monthWeeks, shiftAnchor (clamping month days), snapMinute, and minuteAtPoint are pure and deterministic views, month rows, anchors, snapping, and pointer mapping are deterministic
S5 week view render a role="region" holds a <table role="grid"> labelled by the live heading: day column headers (aria-current="date" on today), time row headers, and one roving tabindex=0 slot button; events render in day columns with --vize-scheduler-top/height/column/columns/span, and all-day events in a labelled lane group with --vize-scheduler-start/end/lane week view renders an APG slot grid, day columns, laid-out events, and all-day lanes
S6 time grid Arrow / Home / End / PageUp / PageDown / Enter ±slot, ±day (RTL flips), first/last slot, ±period; leaving the visible days moves the anchor (update:date); Enter/Space/click emits slot-activate with a { start, end } range slot keyboard navigation moves by slot, day, edges, and period, and activation emits a range
S7 events click / Enter / Alt+Arrow / Alt+Shift+Arrow activation emits the typed event; Alt+Up/Down moves by snapMinutes, Alt+Left/Right by a day, Alt+Shift+Up/Down resizes the end (never below start + snapMinutes); read-only blocks requests and hides resize handles events activate and Alt+Arrow keys request typed moves and resizes
S8 events pointer drag (drag-and-drop) dragging an event to a day column emits event-move snapped to the pointer (grab offset kept, duration preserved); dragging the resize handle emits event-resize ending at the pointer's slot pointer drags move and resize events through drop targets
S9 month view render / keyboard / Alt+Arrow week rows with weekday headers and adjacent-month flags; events beyond maxLanes count into +N; arrows/Home/End/PageUp/PageDown move day focus and cross months; Enter emits day-activate; Alt+Down moves an event a week month view lays out week rows, overflow, keyboard day navigation, and day drops
S10 navigation / API nav buttons / controlled view / expose previous, next, and today buttons page the anchor; controlled view emits update:view only; navigate, goToToday, setDate, setView, and focus are exposed; disabled schedulers disable slots navigation controls, controlled view, pending without a clock, and the exposed API
S11 composition default and part slots the root slot receives SchedulerSlotState<Data> with typed payloads; grid parts accept slot content custom composition exposes typed slot state and individual parts
S12 SSR isolated requests / hydration week, day, and month render byte-identically and hydrate silently; without today/now the server renders a pending shell renders byte-identical week, day, and month markup with deterministic layout, hydrates every view without mismatches, without today or now the server renders a pending shell and hydrates silently

SSR determinism

Layout is a pure function of the events, the anchor date, and props. The anchor comes from date, then defaultDate, then the calendar "today" rules (today, then an injected now + timeZone, then the client clock after mount). Drag-and-drop listeners attach only on the client.

Headless positioning

Consumers position events with CSS custom properties. Time-grid events expose --vize-scheduler-top and --vize-scheduler-height (percent of the day column), plus --vize-scheduler-column, --vize-scheduler-columns, and --vize-scheduler-span. Row events expose --vize-scheduler-start and --vize-scheduler-end (CSS grid lines) and --vize-scheduler-lane. The time grid root also exposes --vize-scheduler-days and --vize-scheduler-slots.