Skip to main content

Key Features

A tour of what the SVAR Svelte Calendar widget does out of the box: the views you can switch between, how users edit and move events, what recurrence and filtering give you, and the hooks for tailoring look and behavior.

full calendar widget overview showing the toolbar on top and a populated week view below

What the Widget Gives You

The Calendar is one Svelte 5 component backed by a reactive store. You hand it an array of events and it renders them in any of seven views, handles the grid math, drag interactions, and toolbar wiring. Every user action - navigating, creating, editing, deleting, filtering - flows through one event bus you can observe or intercept.

Three things to keep in mind:

  • One data shape. Every event has id, start, end, plus any custom fields you carry along (color, project id, calendar group). The same event flows through all views.
  • Views are pluggable. Built-in views are just registered classes. Configure them, add more, or write your own.
  • The store is the API. State (current date, view, filters, selection) and actions (add-event, update-event, ...) are accessed through one instance handle, exposed via bind:this or the init callback.

The rest of this guide groups features by what users see and what you wire up around them.

Views at a Glance

The widget ships with seven views. Only day, week, and month show up by default - list more in the views prop to expose them.

View idLayoutRangePlan
dayTime grid plus an all-day bar1 dayMIT
week7-column time grid plus an all-day bar1 weekMIT
monthCalendar matrix, weeks as rows1 monthMIT
agendaChronological list grouped by day1 monthPRO
year12 mini-months with markers on busy days1 yearPRO
resourcesResource columns over a vertical time axis1 dayPRO
timelineResource rows with a horizontal time axis1 dayPRO

Picking the active view and which ones to show

view selects the view on mount; views controls what the toolbar's view switcher offers.

<Calendar
events={data}
view="week"
views={["day", "week", "month", "agenda", "year"]}
/>

Tweaking a built-in view

views entries can be ViewConfig objects. Use them to relabel a view or deep-merge section overrides. This week view runs from 6 AM to 10 PM and snaps drags to 30 minutes:

<Calendar
events={data}
views={[
{
id: "week",
sections: {
timeGrid: {
yScale: { startHour: 6, endHour: 22, snapStep: 30 },
},
},
},
"month",
]}
/>

The merge replaces only the keys you specify; step, format, and the other section defaults stay put.

Resource scheduling

The resources and timeline views need columns (or rows) defined via xScale.items / yScale.items plus an accessor that maps each event to its column id:

<Calendar
events={data}
view="resources"
views={[
{
id: "resources",
sections: {
timeGrid: {
xScale: {
items: [
{ id: "room-a", label: "Room A" },
{ id: "room-b", label: "Room B" },
],
accessor: "roomId",
},
},
},
},
]}
/>

When a section override isn't enough - a workweek, a fortnight, a fiscal year - subclass ViewModel and call registerCalendarView.

Editing and Interaction

Out of the box, users can:

  • Click and drag on a time grid to create a new event.
  • Drag an event to move it; drag its edges to resize.
  • Click an event to select it; the Editor companion opens automatically when bound to the calendar.
  • Right-click an event for a context menu - once you wrap the calendar in <ContextMenu>.

Every interaction dispatches an action through the store. You can observe or block them by passing prop callbacks (onaddevent, onupdateevent, ondeleteevent, onselectevent) or registering handlers via api.on(...) and api.intercept(...).

Inline editing with the Editor

Editor is a separate companion component. Mount it next to <Calendar> and pass the same api:

<script>
import { Calendar, Editor } from "@svar-ui/svelte-calendar";

let api = $state();
</script>

<Calendar bind:this={api} {events} />
<Editor {api} />

It auto-saves field changes through update-event. Use placement="modal" for a dialog layout, or autoSave={false} plus a custom bottomBar for an explicit Save flow.

Read-only mode

Pass readonly to disable drag, resize, and create, and to drop the toolbar's add-event button:

<Calendar {events} readonly />

Right-click menu

ContextMenu wraps the calendar and adds a right-click menu. The default edit-event and delete-event items are handled internally; custom entries bubble through onclick. getMenuOptions() returns the default item list as a starting point:

<script>
import { Calendar, ContextMenu, getMenuOptions } from "@svar-ui/svelte-calendar";

let api = $state();

const options = [
...getMenuOptions(),
{ id: "duplicate", text: "Duplicate", icon: "wxi-empty" },
];

function onclick({ action, context }) {
if (action.id === "duplicate") {
api.exec("add-event", { event: { ...context, id: undefined } });
}
}
</script>

<ContextMenu {api} {options} {onclick}>
<Calendar bind:this={api} {events} />
</ContextMenu>

Programmatic control

The instance API is the entry point for anything you do without user input - jumping dates, switching views, pushing events from a backend, or blocking a delete with a confirmation dialog. api.exec dispatches actions, api.intercept lets you cancel them:

<script>
let api = $state();

function init(a) {
api = a;
a.intercept("delete-event", ({ id }) => confirm("Delete this event?"));
}

function goToToday() {
api.exec("navigate-time", { direction: "now" });
}
</script>

<Calendar {events} {init} />

See the API reference for the full surface.

Recurrence and Filtering

Recurring events

Set recurring and add an iCalendar rrule to events that repeat. The store expands them into occurrences and supports editing one instance or this-and-following:

<script>
const events = [
{
id: 1,
start: new Date(2026, 4, 4, 9, 0),
end: new Date(2026, 4, 4, 9, 30),
text: "Daily standup",
rrule: "FREQ=DAILY;INTERVAL=1",
},
];
</script>

<Calendar {events} recurring />

When the user edits a recurring event, onupdateevent carries mode: "single" | "following" plus the originalDate of the occurrence. Use those to persist exceptions correctly. Exceptions track back to the master event via masterEventId and originalDate; excluded dates live in exdates.

Filtering events

Filtering is view-level: the source events array isn't touched, only what gets rendered. Dispatch filter-events with a predicate and an optional tag to stack multiple filters:

api.exec("filter-events", {
filter: event => event.priority === "high",
tag: "priority",
});

// Clear by re-dispatching with no filter under the same tag
api.exec("filter-events", { filter: null, tag: "priority" });

For a turnkey calendar-group sidebar, mount CalendarPanel inside the calendar's children snippet - it dispatches filter-events for you when a checkbox toggles:

<script>
import { Calendar, CalendarPanel } from "@svar-ui/svelte-calendar";

const calendars = [
{ id: "work", label: "Work" },
{ id: "personal", label: "Personal", active: false },
];
</script>

<Calendar {events}>
<CalendarPanel {calendars} accessor="calendarId" />
</Calendar>

Customization Hooks

The widget exposes hooks at every layer - markup, styling, toolbar layout, event content, and persistence.

Cell and event styling

cellCss paints background grid cells; eventCss paints event wrappers. Both receive a context with the active view, section, and mode, so a single callback can adapt across layouts:

<script>
function cellCss(ctx) {
if (ctx.date && (ctx.date.getDay() === 0 || ctx.date.getDay() === 6)) {
return "weekend";
}
return "";
}

function eventCss(ctx) {
return `priority-${ctx.event.priority ?? "normal"}`;
}
</script>

<Calendar {events} {cellCss} {eventCss} />

See the styling guide for the full set of mode and section identifiers.

Custom event content, tooltip, and event card

Three slots take Svelte components: eventContent replaces the inner markup of an event, tooltip shows on hover, eventPopup shows on click. Each receives a small set of props (event, mode, sometimes close):

<Calendar
{events}
eventContent={MyEventBlock}
tooltip={MyTooltip}
eventPopup={MyEventCard}
/>

Custom toolbar

Pass toolbar.items to replace the default layout. getToolbarItems() returns the default array as a starting point - append, remove, or swap entries:

<script>
import { getToolbarItems } from "@svar-ui/svelte-calendar";

const toolbar = {
items: [
...getToolbarItems(),
{ id: "export", comp: "button", text: "Export" },
],
};

function onaction({ id }) {
if (id === "export") {
// ...
}
}
</script>

<Calendar {events} {toolbar} {onaction} />

Pass toolbar={null} to hide it entirely. Custom item ids fire action, which the onaction prop and api.on("action", ...) both observe.

Custom editor fields

Editor accepts an items array. Build it from getEditorItems() plus your own entries, and register a Svelte component for any custom comp type via registerEditorItem:

<script>
import { Editor, getEditorItems, registerEditorItem } from "@svar-ui/svelte-calendar";
import Comments from "@wx/svelte-comments";

registerEditorItem("comments", Comments);

const items = [
...getEditorItems(),
{ comp: "comments", key: "comments", label: "Comments" },
];
</script>

<Editor {api} {items} />

Backend integration

For a one-call REST hookup, attach RestDataProvider inside init. It implements the action handlers for add-event, update-event, and delete-event, so user changes round-trip to your server without per-action wiring. Use api.setNext to attach it to the action chain:

<script>
import { Calendar, RestDataProvider } from "@svar-ui/svelte-calendar";

const provider = new RestDataProvider("/api");
let events = $state([]);

$effect(async () => {
events = await provider.getData();
});

function init(api) {
api.setNext(provider);
}
</script>

<Calendar {events} {init} />

For non-REST backends, listen to onaddevent, onupdateevent, and ondeleteevent and persist the changes yourself.

iCal import and export

parseICal turns an .ics payload into a CalendarEvent[] ready to feed into events. serializeICal does the reverse - useful for exporting api.getEvents() as a downloadable file.

import { parseICal, serializeICal } from "@svar-ui/svelte-calendar";

const imported = parseICal(text);
const ics = serializeICal(api.getEvents());

Theming and localization

Wrap the calendar in Willow or WillowDark for the built-in themes. For other languages and a custom week start, use the Locale wrapper from @svar-ui/calendar-locales - see the localization guide.

Where to Go Next