Custom Views
This guide covers how to reshape one of the built-in views with a config override, and how to build a new view by subclassing ViewModel when an override isn't enough.
How views work
A view is a class - a subclass of ViewModel - registered against a string id. The views prop lists the ids that appear in the mode switcher; view picks the active one. The built-ins (day, week, month, agenda, year, resources, timeline) are pre-registered when the package loads.
Each view describes itself through three things:
- Sections - the layout regions returned from
getSections(). A section binds an x-scale, a y-scale, a render mode (bars,boxes,grid,list,year), an optional event filter, and a size. The week view has two sections (multidaybar andtimeGrid); the month view has one (month). - Range math -
rangeStart(date)aligns an arbitrary date to the period start,addRange(date, n)steps bynperiods,setRange(date)writes the resultingstartDateandendDateonto the instance. - Range label -
getRangeLabel()returns the toolbar title for the current range.
The base class owns the rest: it filters events, splits them across scale boundaries, lays them out, and produces the SectionResult[] the renderer draws. You only override the parts you need to change.
There are two ways to customize a view:
- Override sections on a registered view. Pass a
ViewConfigobject inviewswith asectionsmap. The store deep-merges the override into the section of the same name before processing. No new class, no registration call. - Subclass and register. Write a class that extends
ViewModel(or one of the concrete subclasses), then callregisterCalendarView(id, Class)so the id resolves to your class.
Use the override path when the existing view shape is right and only the numbers need changing - visible hours, snap step, resource items. Reach for a subclass when you need a different range size, a different section count, or custom label formatting.
Overriding sections on a built-in view
The views prop accepts strings or ViewConfig objects. The object form lets you relabel a view and merge partial overrides into its sections, keyed by section name:
type ViewConfig =
| string
| {
id: string;
label?: string;
sections?: Record<string, any>;
};
Only the keys you set are replaced. Other fields (step, format, ui, filters) keep their defaults. Plain objects merge recursively; arrays, functions, and primitives replace wholesale.
Tweaking the time axis
Narrow the day view to working hours and add a current-time line:
<Calendar
events={data}
view="day"
views={[
{
id: "day",
sections: {
timeGrid: {
yScale: { startHour: 9, endHour: 17 },
ui: { nowLine: true },
},
},
},
"week",
"month",
]}
/>
Section names come from getSections(): multiday and timeGrid for day/week/resources/timeline, month for the month grid. Override keys that don't match a real section name are ignored.
Defining resource columns
The resources view ships with one placeholder column. Replace it by overriding xScale.items and xScale.accessor:
<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",
},
},
},
},
]}
/>
Each event reads event.roomId to pick a column. Events whose accessor value doesn't match an item id don't render. The timeline view is the same shape transposed - put the resources on yScale instead.
Relabeling without changing sections
Use label to override the mode-switcher text without touching the layout:
const views = [
{ id: "day", label: "Today" },
{ id: "week", label: "This week" },
"month",
];
Subclassing ViewModel
When you need new sections, a different range size, or custom label formatting, subclass the closest built-in view (WeekViewModel, MonthViewModel, etc.) and override what changes. The base pipeline (process(), toPositionStart(), toPositionEnd()) keeps working without you touching it.
The mandatory override points:
| Method | Returns | Purpose |
|---|---|---|
getSections() | Section[] | Declare the view's sections and scales |
rangeStart(date) | Date | Align an arbitrary date to the period start |
addRange(date, n) | Date | Step the date forward or backward by n |
getRangeLabel() | string | Format the toolbar title for the current range |
Inside the class you can read this.startDate, this.endDate, this.weekStartDay, and this.fmt(pattern) for locale-aware formatting.
A work-week variant
Reuse the week layout but show only Monday through Friday by extending WeekViewModel and trimming the xScale.length:
import { WeekViewModel } from "@svar-ui/svelte-calendar";
class WorkWeekViewModel extends WeekViewModel {
getSections() {
const sections = super.getSections();
return sections.map(s => ({
...s,
xScale: { ...s.xScale, length: 5 },
}));
}
rangeStart(date: Date): Date {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
// Always Mon-Fri, regardless of locale's weekStart
const diff = (((d.getDay() - 1) % 7) + 7) % 7;
d.setDate(d.getDate() - diff);
return d;
}
}
addRange is inherited from WeekViewModel (steps by 7 days), which is what we want - Prev/Next still moves one week at a time.
A two-week variant
Extend the visible span to 14 days, drop the multiday bar, and write a custom range label:
import { WeekViewModel } from "@svar-ui/svelte-calendar";
class TwoWeeksViewModel extends WeekViewModel {
getSections() {
const [, days] = super.getSections();
return [
{
...days,
xScale: { ...days.xScale, length: 14 },
boxLayout: "overlap",
},
];
}
addRange(date: Date, n: number): Date {
const d = new Date(date);
d.setDate(d.getDate() + n * 14);
return d;
}
getRangeLabel(): string {
const start = this.startDate;
const end = new Date(this.endDate.getTime() - 1);
const opts: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" };
if (start.getMonth() === end.getMonth()) {
const month = start.toLocaleDateString(undefined, { month: "long" });
return `${month} ${start.getDate()}-${end.getDate()}, ${start.getFullYear()}`;
}
return `${start.toLocaleDateString(undefined, opts)} - ${end.toLocaleDateString(undefined, opts)}, ${end.getFullYear()}`;
}
}
Two things worth flagging. addRange controls the navigation step, so changing the visible span without changing it leaves the user clicking Next and moving only one week through a two-week range. And the second section returned by super.getSections() is the time grid (days here) - index zero is the multiday bar.
Optional overrides
For deeper customization, the base class exposes a few more extension points:
| Member | Purpose |
|---|---|
render = "scrollable" | Use the dedicated single-section scrollable renderer instead of the default |
setRange(date) | Replace the default endDate = addRange(startDate, 1) rule |
process(events) | Bypass the standard pipeline (agenda and year do this) |
buildCells(xScale, yScale) | Produce GridCell[] for grid mode sections |
sortBeforeLayout(primitives, mode) | Reorder primitives before the layout pass runs |
mapToPrimitive(chunk, unit, secScale, axis) | Customize how chunks become primitives |
Registering and using the view
A class becomes a view when you give it an id through registerCalendarView. The id is what views and view reference:
import { registerCalendarView } from "@svar-ui/svelte-calendar";
registerCalendarView("workweek", WorkWeekViewModel);
registerCalendarView("2weeks", TwoWeeksViewModel);
Registration is global. Calling it twice with the same id replaces the previous class for every future Calendar instance - handy for swapping a built-in view, but be deliberate about it.
Then list the new ids in views (with optional labels) and pick one as the initial view:
<script lang="ts">
import { Calendar } from "@svar-ui/svelte-calendar";
const views = [
{ id: "week", label: "Week" },
{ id: "workweek", label: "Work Week" },
{ id: "2weeks", label: "2 Weeks" },
];
</script>
<Calendar {events} {date} view="workweek" {views} />
Call registerCalendarView once at module load, before any Calendar mounts. The store reads the registry when it instantiates a view, so a late registration won't reach instances that already exist.