Skip to main content

BwCalendar | bw-calendar

This component is experimental and it's public api is subject to change

Overview

A full calendar with day, week and month views, multiple named calendars, all-day & recurring (RRULE) events with per-occurrence editing, drag/resize, and ICS export.

The host owns the events array. The calendar emits mutation intents (eventCreate/eventUpdate/eventDelete); apply them with the exported applyEventChange helper and feed the result back through events.

Usage

Basic

bw-calendar manages its own scrolling and overflow, so just give it a height and drop it into any container.

You drive it with two properties (set them in JavaScript, since they're arrays): calendars and events. The calendar renders them and emits intents when the user makes changes — apply those back to your events to make it fully interactive.

The demo below is live: click an empty slot to add an event, click an event to edit it, and drag to reschedule.

<bw-calendar id="quickstart" view="week" style="height: 32rem; width: 100%;" class="elevation-md radius-md"></bw-calendar>
<script>
import { applyEventChange } from 'bluewater';

const cal = document.getElementById('quickstart');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (offsetDays, h, m = 0) => {
const d = new Date(day); d.setDate(d.getDate() + offsetDays); d.setHours(h, m); return d.getTime();
};

cal.calendars = [
{ id: 'work', name: 'Work', color: 'blue' },
{ id: 'home', name: 'Home', color: 'green' },
];
cal.events = [
{ id: '1', title: 'Standup', start: at(0, 9), end: at(0, 9, 30), calendarId: 'work', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: '2', title: 'Lunch', start: at(1, 12), end: at(1, 13), calendarId: 'home' },
{ id: '3', title: 'Conference', start: at(2, 0), end: at(5, 0), allDay: true, calendarId: 'work' },
];

// The calendar emits intents; `applyEventChange` applies them to your events,
// handling recurrence exceptions, overrides, and series splits for you.
let nextId = 0;
cal.addEventListener('eventCreate', e => {
const event = { ...e.detail.event, id: 'new-' + (++nextId) };
cal.events = applyEventChange(cal.events, { ...e.detail, event }, 'create');
});
cal.addEventListener('eventUpdate', e => { cal.events = applyEventChange(cal.events, e.detail, 'update'); });
cal.addEventListener('eventDelete', e => { cal.events = applyEventChange(cal.events, e.detail, 'delete'); });
</script>

Those three handlers are the whole integration. applyEventChange (imported from bluewater) applies each intent — including recurrence exceptions, overrides, and series splits — so deleting or editing a single occurrence of the recurring standup behaves correctly. The rest of these docs cover each capability in depth:

Events and Calendars

bw-calendar is data-driven and stateless about your data: you give it an array of events through the events property and it renders them. It never mutates that array — when the user creates, edits, drags, or deletes something it emits an intent event for you to apply (see Managing Events).

Because events and calendars are arrays, set them as properties in JavaScript rather than as HTML attributes.

The event shape

interface BwCalendarEvent {
id: string; // stable, unique
title: string;
start: number; // epoch milliseconds
end: number; // epoch milliseconds (exclusive)
allDay?: boolean;
calendarId?: string; // links to a BwCalendarSource
location?: string;
notes?: string;
url?: string;
recurrence?: string; // RRULE, e.g. "FREQ=WEEKLY;BYDAY=MO"
color?: string; // overrides the calendar color
readonly?: boolean; // disables drag/resize/edit for this event
}

Times are plain epoch milliseconds (Date.now(), date.getTime()). For an all-day event, start is the first day and end is the day after the last day.

<bw-calendar id="evtCal" view="week" style="height: 30rem; width: 100%;" class="elevation-md radius-md"></bw-calendar>
<script>
const cal = document.getElementById('evtCal');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };
cal.events = [
{ id: '1', title: 'Kickoff', start: at(0, 9), end: at(0, 10), location: 'Room 4' },
{ id: '2', title: 'Focus block', start: at(0, 14), end: at(0, 16), notes: 'No meetings' },
{ id: '3', title: 'Out of office', start: at(1, 0), end: at(3, 0), allDay: true },
];
</script>

Calendars

Group events into named calendars, each with its own color. The calendar renders a sidebar listing them with show/hide checkboxes. An event joins a calendar via its calendarId.

interface BwCalendarSource {
id: string;
name: string;
color: string; // a design-system token ('blue', 'green-600') or any CSS color
hidden?: boolean; // start hidden / toggled off
}

color accepts a design-system token group ('blue'), a token + shade ('blue-600'), or any raw CSS color ('#ff8800', 'tomato').

<bw-calendar id="calCal" view="week" style="height: 30rem; width: 100%;" class="elevation-md radius-md"></bw-calendar>
<script>
const cal = document.getElementById('calCal');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };
cal.calendars = [
{ id: 'work', name: 'Work', color: 'blue' },
{ id: 'personal', name: 'Personal', color: 'green' },
{ id: 'family', name: 'Family', color: '#f5871f' },
];
cal.events = [
{ id: '1', title: 'Standup', start: at(0, 9), end: at(0, 9, 30), calendarId: 'work' },
{ id: '2', title: 'Gym', start: at(0, 18), end: at(0, 19), calendarId: 'personal' },
{ id: '3', title: 'Dinner', start: at(0, 19, 30), end: at(0, 21), calendarId: 'family' },
{ id: '4', title: 'Review (custom color)', start: at(0, 13), end: at(0, 14), calendarId: 'work', color: 'purple' },
];
</script>

If you omit calendars, events fall back to a single implicit blue calendar and no sidebar list is shown. Hide the sidebar entirely with the show-sidebar="false" attribute.

Reacting to clicks

eventClick fires whenever an event is clicked, with the full occurrence in event.detail. By default the calendar then opens its built-in editor — call preventDefault() if you want to handle the click yourself instead.

<div class="flex-column gap" style="width: 100%;">
<bw-calendar id="clickCal" view="week" style="height: 26rem; width: 100%;" class="elevation-md radius-md"></bw-calendar>
<bw-note id="clickOut">Click an event…</bw-note>
</div>
<script>
const cal = document.getElementById('clickCal');
const out = document.getElementById('clickOut');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };
cal.events = [{ id: '1', title: 'Click me', start: at(0, 10), end: at(0, 11) }];
cal.addEventListener('eventClick', e => {
e.preventDefault(); // suppress the built-in editor for this demo
out.textContent = 'Clicked: ' + e.detail.occurrence.event.title;
});
</script>

Exporting to ICS

The calendar can serialize its events to the iCalendar (.ics) format that Apple Calendar, Google Calendar, and Outlook all import. Recurrence rules, exceptions, overrides, and all-day events are all preserved.

Two methods are available:

  • exportICS(opts?) — returns the .ics text as a string.
  • downloadICS(opts?) — builds the file and triggers a browser download.

Both accept an optional { calendarIds } to export only events from specific calendars, and downloadICS also takes a filename (default calendar.ics).

<div class="flex-column gap" style="width: 100%;">
<div class="flex-row gap">
<bw-button id="icsDownload">Download .ics</bw-button>
<bw-button id="icsShow" variant="secondary">Show .ics text</bw-button>
</div>
<bw-calendar id="icsCal" view="week" show-sidebar="false" style="height: 26rem; width: 100%;" class="elevation-md radius-md"></bw-calendar>
<pre
id="icsOut"
style="
max-height: 12rem;
overflow: auto;
background: light-dark(var(--bw-gray-light-50),var(--bw-gray-dark-800));
padding: 0.75rem;
border-radius: var(--bw-radius-md);
font-size: 0.75rem;
white-space: pre-wrap;
">
</pre>
</div>
<script>
const cal = document.getElementById('icsCal');
const out = document.getElementById('icsOut');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };
cal.calendars = [{ id: 'work', name: 'Work', color: 'blue' }];
cal.events = [
{ id: '1', title: 'Standup', start: at(0, 9), end: at(0, 9, 30), calendarId: 'work', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: '2', title: 'Conference', start: at(1, 0), end: at(3, 0), allDay: true, calendarId: 'work' },
];
document.getElementById('icsDownload').addEventListener('click', () => cal.downloadICS({ filename: 'my-calendar.ics' }));
document.getElementById('icsShow').addEventListener('click', async () => { out.textContent = await cal.exportICS(); });
</script>

In code:

const cal = document.querySelector('bw-calendar');

// Whole calendar as a string
const ics = await cal.exportICS();

// Only the "work" calendar, downloaded as work.ics
await cal.downloadICS({ calendarIds: ['work'], filename: 'work.ics' });

Managing Events

The calendar never edits your events array directly. When the user creates, edits, drags, resizes, or deletes an event it emits an intent describing the requested change, and you apply it to your own state. This keeps your data as the single source of truth — wire it to component state, a store, or a server.

There are three intents, all carrying a BwCalendarMutation in event.detail:

EventFired when
eventCreateThe user saves a brand-new event (clicked an empty slot or day).
eventUpdateThe user saved edits, or dragged / resized an event.
eventDeleteThe user deleted an event.
interface BwCalendarMutation {
event: BwCalendarEvent; // the new/edited event
occurrenceStart?: number; // for per-occurrence recurring edits
scope: 'single' | 'occurrence' | 'future' | 'all';
}

A working calendar

The demo below is fully interactive. Click an empty slot to create an event, click an event to edit it, drag it to move, drag its bottom edge to resize, or open it and press Delete. Each change is applied straight back into cal.events, and the two buttons add an event and export everything to an .ics file.

<div class="flex-column gap" style="width: 100%;">
<div class="flex-row gap">
<bw-button id="mgmtAdd">Add event tomorrow</bw-button>
<bw-button id="mgmtIcs" variant="secondary">Export .ics</bw-button>
</div>
<bw-calendar id="mgmtCal" view="week" style="height: 32rem; width: 100%;" class="elevation-md radius-md"></bw-calendar>
</div>
<script>
import { applyEventChange } from 'bluewater';

const cal = document.getElementById('mgmtCal');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };

cal.calendars = [
{ id: 'work', name: 'Work', color: 'blue' },
{ id: 'personal', name: 'Personal', color: 'green' },
];
cal.events = [
{ id: 'e1', title: 'Standup', start: at(0, 9), end: at(0, 9, 30), calendarId: 'work', recurrence: 'FREQ=DAILY' },
{ id: 'e2', title: 'Design review', start: at(0, 11), end: at(0, 12), calendarId: 'work' },
{ id: 'e3', title: 'Lunch', start: at(0, 12, 30), end: at(0, 13, 30), calendarId: 'personal' },
];

// Apply every intent through applyEventChange — single, this-occurrence,
// this-and-future, and all-events scopes are handled for you.
let nextId = 0;
cal.addEventListener('eventCreate', e => {
const event = { ...e.detail.event, id: 'new-' + (++nextId) };
cal.events = applyEventChange(cal.events, { ...e.detail, event }, 'create');
});
cal.addEventListener('eventUpdate', e => { cal.events = applyEventChange(cal.events, e.detail, 'update'); });
cal.addEventListener('eventDelete', e => { cal.events = applyEventChange(cal.events, e.detail, 'delete'); });

// Programmatic create + ICS export via the public methods.
document.getElementById('mgmtAdd').addEventListener('click', () => {
cal.events = [...cal.events, { id: 'new-' + (++nextId), title: 'New event', start: at(1, 14), end: at(1, 15), calendarId: 'work' }];
});
document.getElementById('mgmtIcs').addEventListener('click', () => cal.downloadICS({ filename: 'demo.ics' }));
</script>

All three handlers route through applyEventChange, the helper exported from bluewater. It reads the mutation's scope and occurrenceStart and implements the full iCalendar semantics — so editing or deleting a single instance of the daily standup affects only that day, not the whole series, while "All Events" changes the master.

import { applyEventChange } from 'bluewater';

const cal = document.querySelector('bw-calendar');

cal.addEventListener('eventCreate', e => {
const event = { ...e.detail.event, id: crypto.randomUUID() };
cal.events = applyEventChange(cal.events, { ...e.detail, event }, 'create');
});
cal.addEventListener('eventUpdate', e => { cal.events = applyEventChange(cal.events, e.detail, 'update'); });
cal.addEventListener('eventDelete', e => { cal.events = applyEventChange(cal.events, e.detail, 'delete'); });

applyEventChange(events, mutation, kind, timeZone?) returns a new array and never mutates the input — assign the result straight to cal.events. See Recurring Events for how the scopes behave.

Read-only calendars

Set editable="false" to turn off all creating, editing, dragging, and resizing — the calendar becomes a pure display surface (eventClick still fires). You can also lock an individual event with readonly: true while leaving the rest editable.

<bw-calendar id="roCal" view="week" editable="false" style="height: 26rem; width: 100%;" class="elevation-md radius-md"></bw-calendar>
<script>
const cal = document.getElementById('roCal');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };
cal.events = [
{ id: '1', title: 'Holiday (read-only)', start: at(0, 9), end: at(0, 17) },
{ id: '2', title: 'Locked', start: at(1, 10), end: at(1, 11) },
];
</script>

Recurring Events

Make an event repeat by giving it a recurrence string — a standard RRULE (without the RRULE: prefix). The calendar expands the rule into concrete occurrences for whatever range is visible, anchored to the event's start.

<bw-calendar id="recCal" view="month" show-sidebar="false" style="height: 32rem; width: 100%;" class="elevation-md radius-md"></bw-calendar>
<script>
const cal = document.getElementById('recCal');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };
cal.events = [
{ id: 'standup', title: 'Standup', start: at(0, 9), end: at(0, 9, 15), recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'payday', title: 'Payday', start: at(0, 0), end: at(1, 0), allDay: true, recurrence: 'FREQ=MONTHLY;BYMONTHDAY=1' },
{ id: '1on1', title: '1:1', start: at(0, 15), end: at(0, 15, 30), recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
];
</script>

Common rules:

RepeatsRRULE
Every dayFREQ=DAILY
Every weekdayFREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
Every other weekFREQ=WEEKLY;INTERVAL=2
Monthly on the 1stFREQ=MONTHLY;BYMONTHDAY=1
AnnuallyFREQ=YEARLY
10 times then stopFREQ=WEEKLY;COUNT=10
Until a dateFREQ=WEEKLY;UNTIL=20261231T000000Z

The built-in editor exposes these as a Repeat dropdown (Never / Daily / Weekly / Every Weekday / Monthly / Yearly) plus a Custom… option with frequency, interval, and weekday controls.

Editing one occurrence vs. the whole series

When the user edits, drags, or deletes a recurring occurrence, the calendar asks whether the change applies to this event, this and all future events, or all events, and reports the choice as the scope on the mutation:

  • occurrence — only the one instance (an exception/override).
  • future — this instance and everything after it (splits the series).
  • all — the entire series.

You don't have to interpret these yourself. The exported applyEventChange helper implements the full iCalendar semantics — exceptions (EXDATE), overrides (RECURRENCE-ID), and series splits — and your stored events keep round-tripping cleanly to ICS:

import { applyEventChange } from 'bluewater';

const cal = document.querySelector('bw-calendar');

cal.addEventListener('eventUpdate', e => {
cal.events = applyEventChange(cal.events, e.detail, 'update');
});
cal.addEventListener('eventDelete', e => {
cal.events = applyEventChange(cal.events, e.detail, 'delete');
});

Under the hood this is just data on your events: a deleted occurrence adds its start to the master's recurrenceExceptions, and an edited occurrence becomes a separate override event carrying recurrenceParentId and recurrenceId. You can construct those by hand too, but applyEventChange is the supported path.

The live demo below has the helper wired up — drag or edit one of the daily standups and choose This Event to move just that day.

<bw-calendar id="recEditCal" view="week" show-sidebar="false" style="height: 26rem; width: 100%;" class="elevation-md radius-md"></bw-calendar>
<script>
import { applyEventChange } from 'bluewater';

const cal = document.getElementById('recEditCal');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };
cal.events = [{ id: 'standup', title: 'Standup', start: at(0, 9), end: at(0, 9, 30), recurrence: 'FREQ=DAILY' }];

cal.addEventListener('eventUpdate', e => { cal.events = applyEventChange(cal.events, e.detail, 'update'); });
cal.addEventListener('eventDelete', e => { cal.events = applyEventChange(cal.events, e.detail, 'delete'); });
</script>

Theming

The calendar inherits your theme automatically (it uses the design-system tokens and respects light/dark mode). For finer control it exposes a set of CSS custom properties you can override on the host, and shadow parts you can target with ::part().

Custom properties

PropertyControls
--bw-calendar-bgSurface background
--bw-calendar-grid-lineGrid lines and cell borders
--bw-calendar-today-colorToday's date highlight and the now-line
--bw-calendar-mutedMuted text (weekday labels, times)
--bw-calendar-hour-heightHeight of one hour row in day/week views
--bw-calendar-sidebar-widthSidebar width
--bw-calendar-event-radiusCorner radius of event pills/blocks
<bw-calendar
id="themeCal"
view="week"
show-sidebar="false"
style="
height: 30rem;
width: 100%;
--bw-calendar-today-color: var(--bw-purple-600);
--bw-calendar-hour-height: 4rem;
--bw-calendar-event-radius: 1rem;
--bw-calendar-grid-line: var(--bw-purple-100);
"
class="elevation-md radius-md">
</bw-calendar>
<script>
const cal = document.getElementById('themeCal');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };
cal.calendars = [{ id: 'a', name: 'A', color: 'purple' }];
cal.events = [
{ id: '1', title: 'Taller rows', start: at(0, 9), end: at(0, 10), calendarId: 'a' },
{ id: '2', title: 'Rounder blocks', start: at(0, 11), end: at(0, 12, 30), calendarId: 'a' },
];
</script>

Shadow parts

Target internal regions with ::part() for styling that goes beyond the custom properties:

PartElement
headerThe top toolbar (title, view switcher, nav)
view-switcherThe day/week/month segmented control
sidebarThe sidebar (mini-month + calendar list)
gridThe active view's grid
eventAn event pill/block
event--all-dayAn all-day / multi-day bar
day-cellA day cell in the month grid
all-day-rowThe all-day row in day/week views
now-lineThe current-time indicator line
/* Make the header bold and give all-day bars a heavier weight */
bw-calendar::part(header) {
font-weight: 700;
}
bw-calendar::part(event--all-day) {
font-weight: 600;
}

Time Zones

By default the calendar renders event times in the browser's time zone. Set the time-zone attribute to an IANA zone to render in a fixed zone instead — useful when every user should see a shared schedule (a conference agenda, market hours) in the same zone regardless of where they are.

Your event start/end values stay as plain epoch milliseconds; only the display changes. Recurrence is expanded against the chosen zone, so a 9:00 AM daily event stays at 9:00 AM local even across daylight-saving changes.

<div class="flex-column gap">
<bw-select id="tzSelect" label="Render in time zone" value="America/New_York" style="max-width: 22rem;">
<bw-option value="America/New_York">America/New_York</bw-option>
<bw-option value="America/Los_Angeles">America/Los_Angeles</bw-option>
<bw-option value="Europe/London">Europe/London</bw-option>
<bw-option value="Asia/Tokyo">Asia/Tokyo</bw-option>
</bw-select>
<bw-calendar id="tzCal" view="day" time-zone="America/New_York" show-sidebar="false" style="height: 28rem;" class="elevation-md radius-md"></bw-calendar>
</div>
<script>
const cal = document.getElementById('tzCal');
const sel = document.getElementById('tzSelect');
// A fixed UTC instant — its clock time shifts with the chosen zone.
const base = new Date(); base.setHours(0, 0, 0, 0);
const noonUTC = Date.UTC(base.getFullYear(), base.getMonth(), base.getDate(), 16, 0); // 16:00 UTC
cal.events = [{ id: '1', title: 'Global sync (16:00 UTC)', start: noonUTC, end: noonUTC + 60 * 60 * 1000 }];
sel.addEventListener('valueChange', e => { cal.timeZone = e.detail; });
</script>
// Always show this calendar in Tokyo time
const cal = document.querySelector('bw-calendar');
cal.timeZone = 'Asia/Tokyo';

Views and Navigation

The calendar has three views — month, week, and day — selectable from the segmented control in its header. Set the starting view with the view attribute (default month); it is reflected and two-way, so reading the property always gives the current view and viewChange fires when the user switches.

<bw-calendar id="viewCal" view="day" show-sidebar="false" style="height: 30rem; width: 100%;" class="elevation-md radius-md"></bw-calendar>
<script>
const cal = document.getElementById('viewCal');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };
cal.events = [
{ id: '1', title: 'Standup', start: at(0, 9), end: at(0, 9, 30) },
{ id: '2', title: 'Lunch', start: at(0, 12), end: at(0, 13) },
{ id: '3', title: 'Review', start: at(0, 15), end: at(0, 16) },
];
</script>

The focused date

date is the anchor the view renders around, in epoch milliseconds (default: now). It's two-way — change it to navigate, and dateChange fires when the user uses the header arrows, Today, or the sidebar mini-month.

Drive navigation programmatically with these methods:

  • goToToday()
  • next() / prev() — moves by one day, week, or month depending on the current view
  • goToDate(epochMs)
<div class="flex-column gap">
<div class="flex-row gap">
<bw-button id="navPrev" variant="secondary">‹ Prev</bw-button>
<bw-button id="navToday">Today</bw-button>
<bw-button id="navNext" variant="secondary">Next ›</bw-button>
</div>
<bw-calendar id="navCal" view="week" show-sidebar="false" style="height: 26rem;" class="elevation-md radius-md"></bw-calendar>
</div>
<script>
const cal = document.getElementById('navCal');
document.getElementById('navPrev').addEventListener('click', () => cal.prev());
document.getElementById('navNext').addEventListener('click', () => cal.next());
document.getElementById('navToday').addEventListener('click', () => cal.goToToday());
</script>

Tuning the layout

AttributeDefaultEffect
week-starts-on0 (Sunday)First day of the week (06).
day-start-hour / day-end-hour0 / 24Hour range rendered in the day/week time grids.
scroll-to-hour8Hour scrolled into view on load.
show-sidebartrueShow the mini-month + calendar list sidebar.

The example below starts the week on Monday and only shows working hours (7am–7pm).

<bw-calendar
id="layoutCal"
view="week"
week-starts-on="1"
day-start-hour="7"
day-end-hour="19"
scroll-to-hour="9"
show-sidebar="false"
style="height: 30rem; width: 100%;"
class="elevation-md radius-md">
</bw-calendar>
<script>
const cal = document.getElementById('layoutCal');
const day = new Date(); day.setHours(0, 0, 0, 0);
const at = (o, h, m = 0) => { const d = new Date(day); d.setDate(d.getDate() + o); d.setHours(h, m); return d.getTime(); };
cal.events = [{ id: '1', title: 'Workday', start: at(0, 9), end: at(0, 17) }];
</script>

Lazy-loading events

For large datasets you don't need to hold every event in memory. Listen for rangeChange — it fires with { start, end } (epoch ms) whenever the visible range changes — and fetch just that window.

cal.addEventListener('rangeChange', async e => {
cal.events = await fetchEvents(e.detail.start, e.detail.end);
});

Properties

PropertyAttributeDescriptionTypeDefault
calendars--Named calendars and their colors. If empty, a single implicit calendar is assumed.BwCalendarSource[][]
datedateThe anchor date (epoch ms) that drives which range is shown.numberDate.now()
dayEndHourday-end-hourLast hour rendered in day/week time grids.number24
dayStartHourday-start-hourFirst hour rendered in day/week time grids.number0
editableeditableMaster switch for create/drag/resize/edit/delete.booleantrue
events--The events to display. Owned by the host; the calendar never mutates it.BwCalendarEvent[][]
scrollToHourscroll-to-hourHour scrolled into view on load in day/week.number8
showSidebarshow-sidebarRender the built-in sidebar (mini-month + calendar list).booleantrue
timeZonetime-zoneIANA time zone for rendering. Defaults to the browser zone.stringundefined
viewviewThe active view."day" | "month" | "week"'month'
weekStartsOnweek-starts-onFirst day of the week (0 = Sunday).0 | 1 | 2 | 3 | 4 | 5 | 60

Events

EventDescriptionType
dateChangeEmitted when the anchor date changes.CustomEvent<number>
eventClickEmitted when an event is clicked. Call preventDefault() to suppress the built-in editor.CustomEvent<BwCalendarEventClickDetail>
eventCreateEmitted when an event should be created.CustomEvent<BwCalendarMutation>
eventDeleteEmitted when an event should be deleted.CustomEvent<BwCalendarMutation>
eventUpdateEmitted when an event should be updated.CustomEvent<BwCalendarMutation>
rangeChangeEmitted when the visible range changes (useful for lazy-loading events).CustomEvent<BwCalendarRange>
viewChangeEmitted when the view changes.CustomEvent<"day" | "month" | "week">

Methods

downloadICS(opts?: { calendarIds?: string[]; filename?: string; }) => Promise<void>

Build the ICS and trigger a browser download.

Parameters

NameTypeDescription
opts{ calendarIds?: string[]; filename?: string; }

Returns

Type: Promise<void>

exportICS(opts?: { calendarIds?: string[]; }) => Promise<string>

Build a VCALENDAR string for all events (optionally filtered to calendarIds).

Parameters

NameTypeDescription
opts{ calendarIds?: string[]; }

Returns

Type: Promise<string>

goToDate(date: number) => Promise<void>

Parameters

NameTypeDescription
datenumber

Returns

Type: Promise<void>

goToToday() => Promise<void>

Returns

Type: Promise<void>

next() => Promise<void>

Returns

Type: Promise<void>

prev() => Promise<void>

Returns

Type: Promise<void>

Shadow Parts

PartDescription
"all-day-row"The all-day row in the day/week time grid
"day-cell"A day cell in the month grid
"event"An event pill/block
"event--all-day"An all-day / multi-day event bar
"grid"The active view's grid
"header"The top toolbar
"now-line"The current-time indicator line
"sidebar"The sidebar (mini-month + calendar list)
"view-switcher"The day/week/month segmented control

CSS Custom Properties

NameDescription
--bw-calendar-bgSurface background color
--bw-calendar-date-hHeight of the date number row in month cells
--bw-calendar-event-radiusCorner radius of event pills/blocks
--bw-calendar-grid-lineColor of grid lines and borders
--bw-calendar-gutterWidth of the time gutter in day/week views
--bw-calendar-hour-heightHeight of one hour row in the day/week time grids
--bw-calendar-lane-hHeight of an all-day / multi-day bar lane
--bw-calendar-mutedMuted text color (weekday labels, times)
--bw-calendar-sidebar-widthWidth of the sidebar
--bw-calendar-today-colorAccent for today's date and the now-line (default red)

Dependencies

Depends on

Graph


© 2025 United Systems & Software - All Rights Reserved.