<bmx-timeline>
A timeline: points, ranges and boxes on an axis that zooms from
milliseconds to millennia, in rows of nested groups that open and close.
Overlapping items stack; rows too crowded to stack gather into clusters;
only what is in view is drawn, so a hundred thousand items stay smooth.
Background spans, markers and a live "now" line; items the reader moves,
resizes, adds and deletes when it is editable, snapped to the scale;
every time on the wall clock of a time zone.
22 properties · 5 events · 12 methods · 8 parts
Example
Drag an item to move it (to another row too), drag its ends once selected to resize, double-click empty space to add one, Delete to remove. Ctrl and the wheel zoom; drag the background or the navigator's window to move through time.
History, across three thousand years
Points and ranges from 1200 BC; zoom from millennia down to single years, and the axis follows.
Fifty thousand events, clustered
A day of service events in five rows. Where a row is too crowded to stack, items gather into clusters; click one to zoom in.
Show markup
<bmx-timeline id="ex-tl" editable selectable="multiple" time-zone="Europe/London" label="Product launch plan" style="--bmx-timeline-max-height: 24rem"></bmx-timeline>
<bmx-timeline-navigator for="ex-tl" style="margin-block-start: 0.5rem"></bmx-timeline-navigator>
<p class="note" id="ex-tl-out" role="status">Drag an item to move it (to another row too), drag its ends once selected to resize, double-click empty space to add one, Delete to remove. Ctrl and the wheel zoom; drag the background or the navigator's window to move through time.</p>
<div class="row" style="gap: 0.75rem; flex-wrap: wrap; align-items: center; margin-block: 0.5rem 1.5rem">
<bmx-button variant="outline" tone="neutral" id="ex-tl-fit">Show everything</bmx-button>
<bmx-button variant="outline" tone="neutral" id="ex-tl-week">Show the launch week</bmx-button>
<label><input type="checkbox" id="ex-tl-stack" checked> Stack overlapping items</label>
</div>
<h3>History, across three thousand years</h3>
<p class="note">Points and ranges from 1200 BC; zoom from millennia down to single years, and the axis follows.</p>
<bmx-timeline id="ex-tl-history" time-zone="UTC" show-now="false" label="Ancient and medieval history" style="--bmx-timeline-max-height: 18rem"></bmx-timeline>
<h3>Fifty thousand events, clustered</h3>
<p class="note">A day of service events in five rows. Where a row is too crowded to stack, items gather into clusters; click one to zoom in.</p>
<bmx-timeline id="ex-tl-busy" time-zone="UTC" label="Service events" show-now="false" style="--bmx-timeline-max-height: 20rem"></bmx-timeline>
<script type="module">
await customElements.whenDefined('bmx-timeline');
const tl = document.getElementById('ex-tl');
tl.groups = [
{ id: 'product', content: 'Product' },
{ id: 'design', content: 'Design', parent: 'product' },
{ id: 'build', content: 'Engineering', parent: 'product' },
{ id: 'marketing', content: 'Marketing' },
{ id: 'press', content: 'Press', parent: 'marketing', collapsed: false },
{ id: 'events', content: 'Events', parent: 'marketing' },
];
tl.items = [
{ id: 'disc', content: 'Discovery', start: '2026-09-01', end: '2026-09-19', group: 'product' },
{ id: 'wire', content: 'Wireframes', start: '2026-09-14', end: '2026-09-26', group: 'design' },
{ id: 'ui', content: 'Visual design', start: '2026-09-24', end: '2026-10-10', group: 'design', title: 'Two rounds of review' },
{ id: 'api', content: 'API', start: '2026-09-21', end: '2026-10-17', group: 'build' },
{ id: 'web', content: 'Web app', start: '2026-10-01', end: '2026-10-28', group: 'build' },
{ id: 'qa', content: 'Testing', start: '2026-10-19', end: '2026-11-04', group: 'build', color: '#2b8a3e' },
{ id: 'freeze', content: 'Code freeze', start: '2026-10-29', end: '2026-11-02', type: 'background', group: 'build' },
{ id: 'brief', content: 'Press brief', start: '2026-10-20T10:00', group: 'press', type: 'box' },
{ id: 'embargo', content: 'Embargo lifts', start: '2026-11-05T09:00', group: 'press' },
{ id: 'launch', content: 'Launch event', start: '2026-11-05T18:00', end: '2026-11-05T22:00', group: 'events', color: '#c92a2a' },
{ id: 'webinar', content: 'Webinar', start: '2026-11-12T15:00', end: '2026-11-12T16:00', group: 'events' },
{ id: 'blog', content: 'Launch blog post', start: '2026-11-05T08:00', group: 'marketing' },
];
tl.markers = [{ time: '2026-11-05', label: 'Launch' }];
const out = document.getElementById('ex-tl-out');
tl.addEventListener('bmxTimelineChange', e => (out.textContent = `${e.detail.kind}: ${e.detail.item.content} - ${e.detail.item.start}${e.detail.item.end ? ` to ${e.detail.item.end}` : ''}`));
tl.addEventListener('bmxTimelineSelect', e => (out.textContent = e.detail.ids.length ? `Selected: ${e.detail.ids.join(', ')}` : 'Nothing selected.'));
document.getElementById('ex-tl-fit').addEventListener('click', () => tl.fit());
document.getElementById('ex-tl-week').addEventListener('click', () => tl.setWindow('2026-11-02', '2026-11-09', true));
document.getElementById('ex-tl-stack').addEventListener('change', e => (tl.stack = e.target.checked));
const history = document.getElementById('ex-tl-history');
const year = y => `${y < 0 ? '-' : ''}${String(Math.abs(y)).padStart(4, '0')}-01-01T00:00:00Z`;
const bc = y => new Date(Date.UTC(2000, 0, 1)).setUTCFullYear(1 - y);
history.groups = [{ id: 'eras', content: 'Eras' }, { id: 'events', content: 'Events' }];
history.items = [
{ content: 'Iron Age in the Near East', start: bc(1200), end: bc(550), group: 'eras' },
{ content: 'Classical Greece', start: bc(510), end: bc(323), group: 'eras' },
{ content: 'Roman Empire', start: bc(27), end: year(476), group: 'eras' },
{ content: 'Byzantine Empire', start: year(395), end: year(1453), group: 'eras' },
{ content: 'Middle Ages in Europe', start: year(500), end: year(1500), group: 'eras' },
{ content: 'First Olympic Games recorded', start: bc(776), group: 'events' },
{ content: 'Founding of Rome (traditional)', start: bc(753), group: 'events' },
{ content: 'Battle of Marathon', start: bc(490), group: 'events' },
{ content: 'Death of Julius Caesar', start: bc(44), group: 'events' },
{ content: 'Hadrian’s Wall begun', start: year(122), group: 'events' },
{ content: 'Fall of the Western Roman Empire', start: year(476), group: 'events' },
{ content: 'Domesday Book', start: year(1086), group: 'events' },
{ content: 'Magna Carta', start: year(1215), group: 'events' },
];
const busy = document.getElementById('ex-tl-busy');
let seed = 3;
const rand = () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646;
const day = Date.UTC(2026, 9, 6);
const rows = ['Web', 'API', 'Database', 'Queue', 'Cache'];
busy.groups = rows.map(r => ({ id: r, content: r }));
const events = [];
for (let i = 0; i < 50000; i++) {
const r = rows[Math.floor(rand() * rows.length)];
const t = day + Math.floor(Math.pow(rand(), 1.5) * 86400000);
events.push(rand() < 0.15 ? { id: `e${i}`, content: `Job ${i}`, start: t, end: t + Math.floor(rand() * 600000), group: r } : { id: `e${i}`, content: `${r} event ${i}`, start: t, group: r });
}
busy.items = events;
</script>
Keyboard: the timeline is one stop. The left and right arrows move from item to item in a row, up and down between rows; Enter or Space selects; Shift and the arrows move an editable item (Ctrl as well resizes it); Delete removes it; Page Up and Page Down move through time; + and - zoom; 0 shows everything; N goes to now; Escape clears the selection.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
cluster |
cluster |
boolean |
true |
Rows too crowded to stack gather their items into clusters. |
editable |
editable |
boolean |
false |
The reader may move, resize, add (double-click) and delete items. An item with editable: false stays put. |
end |
end |
number | string | Date |
— | The end of the window in view. |
groups |
groups |
BmxTimelineGroup[] | string |
— | The rows: [{ id, content, parent, collapsed }], as a property or JSON. Without groups, everything is in one row. |
items |
items |
BmxTimelineItem[] | string |
— | The items: [{ id, content, start, end, group, type, color, title }], as a property or JSON. |
label |
label |
string |
— | What the timeline shows: its accessible name. |
locale |
locale |
string |
— | A BCP 47 locale for times. Default: the page's lang. |
markers |
markers |
BmxTimelineMarker[] | string |
— | Lines at moments: [{ time, label, color }], as a property or JSON. |
max |
max |
number | string | Date |
— | The latest time the reader may move to. |
maxLanes |
max-lanes |
number |
6 |
The most lanes a row stacks into before it clusters. |
min |
min |
number | string | Date |
— | The earliest time the reader may move to. |
selectable |
selectable |
'none' | 'single' | 'multiple' |
'single' |
Items the reader can select: none, single (default) or multiple. |
selected |
selected |
string[] | string |
[] |
The selected items' ids. Changes write back here. |
showNow |
show-now |
boolean |
true |
Show a line at the current time. |
snap |
snap |
'auto' | 'none' |
'auto' |
While moving and resizing, times snap to the axis's step (auto), or not (none). Alt held does not snap. |
stack |
stack |
boolean |
true |
Overlapping items are stacked; otherwise they overlap. |
start |
start |
number | string | Date |
— | The start of the window in view: a time as items take it. Default: the items' extent. |
strings |
strings |
Partial<BmxTimelineStrings> | string |
— | Replacements for the timeline's wording, as a property or JSON. |
tableView |
table-view |
boolean |
true |
Offer the table view. |
timeZone |
time-zone |
string |
— | The time zone times are read and shown in (an IANA name). Default: the reader's. |
zoomMax |
zoom-max |
number |
3.15576e14 |
The widest window, in milliseconds (default ten thousand years). |
zoomMin |
zoom-min |
number |
10 |
The narrowest window, in milliseconds. |
Events
| Event | Detail | Description |
|---|---|---|
bmxTimelineChange |
BmxTimelineChangeDetail |
An item was added, moved, resized or removed by the reader. Cancelable: cancelling undoes it. |
bmxTimelineGroupToggle |
BmxTimelineGroupDetail |
A group was opened or closed. |
bmxTimelineItemClick |
BmxTimelineItemDetail |
An item was clicked, or chosen with the keyboard. |
bmxTimelineRangeChange |
BmxTimelineRangeDetail |
The window in view moved or zoomed. |
bmxTimelineSelect |
BmxTimelineSelectDetail |
Items were selected or unselected. |
Methods
| Method | Signature | Description |
|---|---|---|
addItem |
addItem(item: BmxTimelineItem) => Promise<void> |
Adds an item (or replaces the one with its id). |
fit |
fit(ids?: string[]) => Promise<void> |
Shows every item (or the ones given). |
focusItem |
focusItem(id: string, select?: boolean) => Promise<void> |
Brings an item into view and makes it the current one (and selects it when select). |
getDensity |
getDensity(bins?: number, start?: number, end?: number) => Promise<number[]> |
How many items overlap each of bins equal steps from start to end (the items' extent by default). |
getExtent |
getExtent() => Promise<BmxTimelineRangeDetail | null> |
The earliest start and latest end of the items, or null when there are none. |
getItems |
getItems() => Promise<BmxTimelineItem[]> |
Every item as it now is, with the reader's changes, in the shapes the page gave. |
getWindow |
getWindow() => Promise<BmxTimelineRangeDetail> |
The window in view, in milliseconds. |
moveTo |
moveTo(time: unknown, animate?: boolean) => Promise<void> |
Centres the view on a time, keeping the zoom. |
removeItem |
removeItem(id: string) => Promise<void> |
Takes an item out. |
setGroupCollapsed |
setGroupCollapsed(id: string, collapsed: boolean) => Promise<void> |
Opens or closes a group (collapsed true closes it). |
setWindow |
setWindow(start: unknown, end: unknown, animate?: boolean) => Promise<void> |
Moves the view to a window of time. animate eases there. |
updateItem |
updateItem(id: string, changes: Partial<BmxTimelineItem>) => Promise<void> |
Changes an item's fields. |
CSS shadow parts
| Part | Description |
|---|---|
axis |
the time axis. |
cluster |
a cluster of items. |
controls |
the zoom, fit, now and table buttons. |
item |
an item. |
labels |
the group names. |
marker |
a marker line. |
now |
the line at the current time. |
table |
the table view. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-timeline-background |
A background span's colour. |
--bmx-timeline-height |
How tall the rows' area is. Default: as tall as the rows, up to the maximum. |
--bmx-timeline-item |
An item's colour (each item's own color stands over it). |
--bmx-timeline-label-width |
The width of the group names. Default: 11rem. |
--bmx-timeline-marker |
A marker line. |
--bmx-timeline-max-height |
The most the rows' area grows to before it scrolls. Default: 26rem. |
--bmx-timeline-now |
The line at the current time. |