<bmx-scheduler>
A resource scheduler: rooms, people and equipment down the side, time across, and bookings on them - made by dragging over free time, moved and resized by dragging or from the keyboard, and checked as they are made. Day, week and month timelines, and an agenda list (which a narrow screen gets on its own).
31 properties · 6 events · 21 methods · 21 parts
Example
Show markup
<bmx-scheduler
id="ex-sch"
label="Office bookings"
view="week"
day-start="07:00"
day-end="20:00"
holidays="2026-12-25,2026-12-28"
style="--bmx-scheduler-height: 30rem"
></bmx-scheduler>
<div class="row" style="margin-block-start: 1rem">
<span class="note" id="ex-sch-out">
<strong>Drag over free time to book it</strong>, drag a booking to move it (onto another row to change who or
what it books), drag its ends to resize it, and double-click to edit. Overlaps beyond what a resource can hold are
outlined as clashes; shaded time is outside working hours. Try <em>Find a time</em> for the first hour the board
room, Asha and Ben are all free.
</span>
</div>
<script type="module">
await customElements.whenDefined('bmx-scheduler');
const scheduler = document.getElementById('ex-sch');
const out = document.getElementById('ex-sch-out');
// This week's Monday, so the example always shows bookings.
const today = new Date();
const monday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - ((today.getDay() + 6) % 7));
const day = n => {
const d = new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + n);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
};
scheduler.resources = [
{ id: 'board', name: 'Board room', group: 'Rooms', details: '12 seats', color: '#1971c2' },
{ id: 'lab', name: 'Training lab', group: 'Rooms', details: '3 bays', maxConcurrent: 3, color: '#2f9e44',
unavailable: [{ start: `${day(2)}T12:00`, end: `${day(2)}T14:00`, title: 'Deep clean' }] },
{ id: 'asha', name: 'Asha Rao', group: 'People', details: 'Bengaluru', timeZone: 'Asia/Kolkata', workingHours: 'Mon-Fri 09:30-18:00', color: '#9c36b5' },
{ id: 'ben', name: 'Ben Hughes', group: 'People', details: 'Engineer', workingHours: 'Mon-Thu 08:00-16:00', color: '#e8590c' },
{ id: 'van', name: 'Delivery van', group: 'Equipment', color: '#0c8599', workingHours: 'Mon-Sat 07:00-19:00' },
];
scheduler.events = [
{ id: 'standup', title: 'Stand-up', start: `${day(0)}T09:30`, end: `${day(0)}T09:45`, resourceIds: ['board', 'ben'],
recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Quarterly review', start: `${day(1)}T10:00`, end: `${day(1)}T12:00`, resourceIds: ['board', 'asha', 'ben'] },
{ id: 'clash', title: 'Supplier call', start: `${day(1)}T11:00`, end: `${day(1)}T11:45`, resourceId: 'board' },
{ id: 'course', title: 'Safety course', start: `${day(2)}T09:00`, end: `${day(2)}T16:00`, resourceId: 'lab' },
{ id: 'mentor', title: 'Mentoring', start: `${day(3)}T14:00`, end: `${day(3)}T15:00`, resourceIds: ['asha', 'ben'], status: 'tentative' },
{ id: 'round', title: 'Delivery round', start: `${day(0)}T08:00`, end: `${day(0)}T12:30`, resourceId: 'van',
recurrence: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12' },
{ id: 'leave', title: 'Annual leave', start: day(4), end: day(5), allDay: true, resourceId: 'asha' },
];
scheduler.addEventListener('bmxSchedulerChange', event => {
out.textContent = `bmxSchedulerChange - ${event.detail.action}; ${event.detail.events.length} bookings to save.`;
});
scheduler.addEventListener('bmxSchedulerEventChange', event => {
if (event.detail.conflicts.length) out.textContent = `bmxSchedulerEventChange - this would clash (${event.detail.conflicts.length}).`;
});
</script>
DATA
resources are things that can be booked, each with an id, a name,
an optional group (Rooms, Engineers), maxConcurrent (how many
bookings at once - more is a clash), workingHours (Mon-Fri 09:00-17:00),
timeZone and unavailable times. events are bookings, each with an
id, a title, start and end (2026-10-05T09:00 is that wall time,
...Z an instant, 2026-10-05 a whole day), and resourceId - or
resourceIds for a booking that takes a room and two people at once.
Changes come back in bmxSchedulerChange as the whole list, written in
the forms they came in.
REPEATING
recurrence is an iCalendar rule - FREQ=WEEKLY;BYDAY=MO,WE - so series
go to and from any calendar program. Moving or editing one occurrence asks
whether to change it alone, it and the following ones, or the whole series
(or follows recurrence-edits).
CHECKS
Bookings over a resource's limit, or in its unavailable time, are clashes:
outlined, counted in the status bar, and - with conflicts="prevent" -
refused. outside-hours does the same for bookings outside working hours,
which are shaded. Each resource shows its utilisation: booked working time
over working time in the range.
TIME ZONES
Everything is drawn in time-zone (the reader's by default). A resource
with its own zone has its working hours read in it, and shows its local
time. Clock changes are handled: a weekly 09:00 meeting stays at 09:00.
MORE
"Find a time" finds the earliest time a set of resources are free together
(or any one of them); findSlot() does it from script. toICS() and the
toolbar's export write an .ics file; loadICS() reads one. Undo and redo
cover every change. The built-in editor can be replaced by cancelling
bmxSchedulerEventActivate.
KEYBOARD
Tab reaches the bookings; arrow keys move between them (left and right in a row, up and down between rows); Enter opens one; Delete removes it. Alt+arrows move a booking by a slot or to the next resource, and Alt+Shift+Left/Right make it shorter or longer. Page Up and Page Down go to the previous or next range; Ctrl+Z and Ctrl+Y undo and redo.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
conflicts |
conflicts |
'allow' | 'warn' | 'prevent' |
'warn' |
Clashes: allow (not shown), warn (shown and announced) or prevent (refused). |
currentTime |
current-time |
string |
— | The moment taken as now (ISO), for repeatable pages and tests. Default: the clock. |
date |
date |
string |
— | A date in the range on show (2026-10-05). Default: today. |
dayEnd |
day-end |
string |
'24:00' |
See day-start. |
dayStart |
day-start |
string |
'00:00' |
The part of each day drawn in the day and week views: 07:00 to 20:00, say. |
dayWidth |
day-width |
number |
56 |
Pixels per day in the month view. |
defaultDuration |
default-duration |
number |
60 |
Minutes a booking lasts when made by a double-click, or given without an end. |
editor |
editor |
boolean |
true |
Whether double-click and Enter open the built-in editor. |
events |
events |
BmxSchedulerEvent[] | string |
[] |
Bookings: [{ id, title, start, end?, resourceId | resourceIds, recurrence?, ... }], as a property or JSON. Changes write back here. |
firstDayOfWeek |
first-day-of-week |
number |
— | 0 for Sunday to 6 for Saturday. Default: the locale's. |
groupBy |
group-by |
string |
'group' |
The resource field resources are grouped by; empty for no groups. |
holidays |
holidays |
string[] | string |
[] |
Dates no one works (2026-12-25): a list, or text separated by commas. |
hourWidth |
hour-width |
number |
— | Pixels per hour in the day and week views. Default: 72 for a day, 16 for a week. |
label |
label |
string |
— | An accessible name for the scheduler. |
locale |
locale |
string |
— | A BCP 47 locale for dates and times. Default: the page's. |
outsideHours |
outside-hours |
'allow' | 'warn' | 'prevent' |
'allow' |
Bookings outside working hours: allow, warn or prevent. |
readonly |
readonly |
boolean |
false |
No making, moving, resizing or deleting. |
recurrenceEdits |
recurrence-edits |
'ask' | 'occurrence' | 'series' |
'ask' |
Changing one occurrence of a series: ask, or always the occurrence or the series. |
requestInit |
request-init |
RequestInit | string |
— | Options for the src request (headers, credentials), as a property or JSON. |
resourceWidth |
resource-width |
number |
220 |
The width of the resource column, in pixels. |
resources |
resources |
BmxSchedulerResource[] | string |
[] |
Things that can be booked: [{ id, name, group?, maxConcurrent?, workingHours?, timeZone?, unavailable? }], as a property or JSON. |
responsive |
responsive |
boolean |
true |
A narrow scheduler (under 640 pixels) shows the agenda. |
rowHeight |
row-height |
number |
44 |
The least a row is high, in pixels. Rows grow to hold overlapping bookings. |
showUtilisation |
show-utilisation |
boolean |
true |
Whether each resource shows its utilisation. |
slotMinutes |
slot-minutes |
number |
15 |
The step bookings snap to, in minutes. |
src |
src |
string |
— | A URL to load { resources, events } from as JSON, or bookings from an .ics file. |
strings |
strings |
Partial<BmxSchedulerStrings> | string |
— | Words to show instead of the English ones, as a property or JSON. |
timeZone |
time-zone |
string |
— | The zone everything is drawn in (IANA name: Europe/London). Default: the reader's. |
view |
view |
BmxSchedulerView |
'week' |
day, week, month or agenda. |
workWeek |
work-week |
boolean |
false |
The week view shows only working days (those in working-hours). |
workingHours |
working-hours |
string | BmxSchedulerWorkingSpan[] |
'Mon-Fri 09:00-17:00' |
When resources work, unless they say otherwise: text like Mon-Fri 09:00-17:00, or spans. |
Events
| Event | Detail | Description |
|---|---|---|
bmxSchedulerChange |
BmxSchedulerChangeDetail |
Bookings changed: the whole new list. |
bmxSchedulerEventActivate |
BmxSchedulerActivateDetail |
A booking was opened. Cancel it to show your own editor. |
bmxSchedulerEventChange |
BmxSchedulerEventChangeDetail |
A booking is about to change. Cancel it to refuse the change. |
bmxSchedulerEventCreate |
BmxSchedulerCreateDetail |
A reader asked for a new booking. Cancel it to make your own. |
bmxSchedulerRangeChange |
BmxSchedulerRangeChangeDetail |
The range on show changed. |
bmxSchedulerSelect |
BmxSchedulerSelectDetail |
The chosen booking changed. |
Methods
| Method | Signature | Description |
|---|---|---|
addEvent |
addEvent(event: BmxSchedulerEvent) => Promise<boolean> |
Adds a booking. False when it is refused (by a cancelled bmxSchedulerEventChange or a clash under conflicts="prevent"). |
downloadICS |
downloadICS(fileName?: string) => Promise<void> |
Saves every booking as an .ics file through the browser's download. |
findSlot |
findSlot(request: Omit<BmxSchedulerSlotRequest, "from" | "to"> & { from?: string | number | Date; to?: string | number | Date; }) => Promise<{ start: string; end: string; resourceIds: string[]; } | null> |
The earliest time resources are free for a booking: all of them, or any one with match: 'any', in working hours unless workingHoursOnly is false. Times come back as ISO instants. Null when there is none in the next 90 days (or before to). |
getConflicts |
getConflicts(from?: string | number | Date, to?: string | number | Date) => Promise<BmxSchedulerConflict[]> |
The clashes between two times (default: the range on show). |
getEvents |
getEvents() => Promise<BmxSchedulerEvent[]> |
Every booking, as the page would save it. |
getOccurrences |
getOccurrences(from?: string | number | Date, to?: string | number | Date) => Promise<BmxSchedulerOccurrence[]> |
The bookings between two times (default: the range on show), series expanded into occurrences. |
getUtilisation |
getUtilisation(from?: string | number | Date, to?: string | number | Date) => Promise<BmxSchedulerUtilisation[]> |
Each resource's utilisation between two times (default: the range on show). |
goToDate |
goToDate(date: string | Date) => Promise<void> |
Shows the range around a date. |
load |
load(url: string) => Promise<void> |
Loads { resources, events } as JSON, or bookings from an .ics file, from a URL. |
loadICS |
loadICS(source: string | Blob, merge?: boolean) => Promise<number> |
Reads bookings from an .ics file (its text or a File), replacing them, or adding with merge. |
next |
next() => Promise<void> |
The next range. |
openEditor |
openEditor(id?: string) => Promise<void> |
Opens the editor on a booking (by id), or on a new one. |
previous |
previous() => Promise<void> |
The previous range. |
redo |
redo() => Promise<boolean> |
|
removeEvent |
removeEvent(id: string) => Promise<boolean> |
Removes a booking (a whole series, for a repeating one). |
scrollToTime |
scrollToTime(time: string | number | Date) => Promise<void> |
Scrolls the timeline to a time (09:00 on the range's first day, or an instant). |
setFocus |
setFocus() => Promise<void> |
Moves focus to the chosen booking, or the first one on show. |
toICS |
toICS(name?: string) => Promise<string> |
Every booking as an iCalendar (.ics) file's text. |
today |
today() => Promise<void> |
The range with today in it, scrolled to now. |
undo |
undo() => Promise<boolean> |
|
updateEvent |
updateEvent(id: string, changes: Partial<BmxSchedulerEvent>) => Promise<boolean> |
Changes a booking's fields. False when there is no such booking or the change is refused. |
CSS shadow parts
| Part | Description |
|---|---|
agenda |
the agenda list. |
blocked |
time a resource is unavailable. |
dialog |
the editor, the "which occurrences" question and the time finder. |
event |
a booking. Also event-selected, event-clash, event-tentative, event-cancelled, event-recurring. |
ghost |
a booking being dragged, or the time being dragged over to make one. |
group |
a group's row. Also group-collapsed. |
handle |
a booking's resize grip. |
header |
the dates and hours across the top. |
lane |
a row's timeline. |
non-working |
time outside working hours. |
now |
the line at the current time. |
range-label |
the dates on show. |
resource |
a resource's cell at the start of its row. |
row |
a resource's row. |
scroller |
the scrolling timeline. |
status |
the status bar under the timeline. |
tick |
one date or hour in it. Also tick-today. |
tick-hour |
|
toolbar |
the navigation, view and tool buttons. |
toolbar-button |
one of them. |
utilisation |
a resource's utilisation bar. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-scheduler-blocked-color |
The stripes over time a resource is unavailable. |
--bmx-scheduler-border |
The lines between rows, days and hours. |
--bmx-scheduler-clash-color |
The outline and mark of a clash. |
--bmx-scheduler-event-background |
A booking without a colour of its own. |
--bmx-scheduler-event-color |
Text on a booking. |
--bmx-scheduler-event-radius |
A booking's corners. |
--bmx-scheduler-header-background |
The dates and hours across the top, and the resource column's heading. |
--bmx-scheduler-height |
How tall the timeline is (the toolbar and status bar add to it). |
--bmx-scheduler-non-working-background |
Time outside working hours. |
--bmx-scheduler-now-color |
The line at the current time. |
--bmx-scheduler-resource-background |
The resource column. |
--bmx-scheduler-today-background |
Today's column in the header. |