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 — the data model and colors
- Managing Events — creating, editing, dragging, deleting, and
applyEventChange - Recurring Events — RRULE rules and per-occurrence edits
- Views and Navigation — views, the focused date, and layout options
- Exporting to ICS —
.icsexport and download - Time Zones — rendering in a fixed zone
- Theming — CSS custom properties and shadow parts
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.icstext 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:
| Event | Fired when |
|---|---|
eventCreate | The user saves a brand-new event (clicked an empty slot or day). |
eventUpdate | The user saved edits, or dragged / resized an event. |
eventDelete | The 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:
| Repeats | RRULE |
|---|---|
| Every day | FREQ=DAILY |
| Every weekday | FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR |
| Every other week | FREQ=WEEKLY;INTERVAL=2 |
| Monthly on the 1st | FREQ=MONTHLY;BYMONTHDAY=1 |
| Annually | FREQ=YEARLY |
| 10 times then stop | FREQ=WEEKLY;COUNT=10 |
| Until a date | FREQ=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
| Property | Controls |
|---|---|
--bw-calendar-bg | Surface background |
--bw-calendar-grid-line | Grid lines and cell borders |
--bw-calendar-today-color | Today's date highlight and the now-line |
--bw-calendar-muted | Muted text (weekday labels, times) |
--bw-calendar-hour-height | Height of one hour row in day/week views |
--bw-calendar-sidebar-width | Sidebar width |
--bw-calendar-event-radius | Corner 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:
| Part | Element |
|---|---|
header | The top toolbar (title, view switcher, nav) |
view-switcher | The day/week/month segmented control |
sidebar | The sidebar (mini-month + calendar list) |
grid | The active view's grid |
event | An event pill/block |
event--all-day | An all-day / multi-day bar |
day-cell | A day cell in the month grid |
all-day-row | The all-day row in day/week views |
now-line | The 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.
Navigation methods
Drive navigation programmatically with these methods:
goToToday()next()/prev()— moves by one day, week, or month depending on the current viewgoToDate(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
| Attribute | Default | Effect |
|---|---|---|
week-starts-on | 0 (Sunday) | First day of the week (0–6). |
day-start-hour / day-end-hour | 0 / 24 | Hour range rendered in the day/week time grids. |
scroll-to-hour | 8 | Hour scrolled into view on load. |
show-sidebar | true | Show 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
| Property | Attribute | Description | Type | Default |
|---|---|---|---|---|
calendars | -- | Named calendars and their colors. If empty, a single implicit calendar is assumed. | BwCalendarSource[] | [] |
date | date | The anchor date (epoch ms) that drives which range is shown. | number | Date.now() |
dayEndHour | day-end-hour | Last hour rendered in day/week time grids. | number | 24 |
dayStartHour | day-start-hour | First hour rendered in day/week time grids. | number | 0 |
editable | editable | Master switch for create/drag/resize/edit/delete. | boolean | true |
events | -- | The events to display. Owned by the host; the calendar never mutates it. | BwCalendarEvent[] | [] |
scrollToHour | scroll-to-hour | Hour scrolled into view on load in day/week. | number | 8 |
showSidebar | show-sidebar | Render the built-in sidebar (mini-month + calendar list). | boolean | true |
timeZone | time-zone | IANA time zone for rendering. Defaults to the browser zone. | string | undefined |
view | view | The active view. | "day" | "month" | "week" | 'month' |
weekStartsOn | week-starts-on | First day of the week (0 = Sunday). | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 0 |
Events
| Event | Description | Type |
|---|---|---|
dateChange | Emitted when the anchor date changes. | CustomEvent<number> |
eventClick | Emitted when an event is clicked. Call preventDefault() to suppress the built-in editor. | CustomEvent<BwCalendarEventClickDetail> |
eventCreate | Emitted when an event should be created. | CustomEvent<BwCalendarMutation> |
eventDelete | Emitted when an event should be deleted. | CustomEvent<BwCalendarMutation> |
eventUpdate | Emitted when an event should be updated. | CustomEvent<BwCalendarMutation> |
rangeChange | Emitted when the visible range changes (useful for lazy-loading events). | CustomEvent<BwCalendarRange> |
viewChange | Emitted 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
| Name | Type | Description |
|---|---|---|
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
| Name | Type | Description |
|---|---|---|
opts | { calendarIds?: string[]; } |
Returns
Type: Promise<string>
goToDate(date: number) => Promise<void>
Parameters
| Name | Type | Description |
|---|---|---|
date | number |
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
| Part | Description |
|---|---|
"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
| Name | Description |
|---|---|
--bw-calendar-bg | Surface background color |
--bw-calendar-date-h | Height of the date number row in month cells |
--bw-calendar-event-radius | Corner radius of event pills/blocks |
--bw-calendar-grid-line | Color of grid lines and borders |
--bw-calendar-gutter | Width of the time gutter in day/week views |
--bw-calendar-hour-height | Height of one hour row in the day/week time grids |
--bw-calendar-lane-h | Height of an all-day / multi-day bar lane |
--bw-calendar-muted | Muted text color (weekday labels, times) |
--bw-calendar-sidebar-width | Width of the sidebar |
--bw-calendar-today-color | Accent for today's date and the now-line (default red) |
Dependencies
Depends on
- bw-tabs
- bw-tab-button
- bw-icon-button
- bw-button
- bw-overlay
- bw-view
- bw-checkbox
- bw-datepicker
- bw-input
- bw-toggle
- bw-select
- bw-option
Graph
© 2025 United Systems & Software - All Rights Reserved.