v2.0.0

<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.

Show everything Show the launch week

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>

timeline.groups = [{ id: 'web', content: 'Web team' }]; timeline.items = [{ id: 1, content: 'Design review', start: '2026-10-05T09:00', end: '2026-10-05T11:00', group: 'web' }];

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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

PartDescription
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

PropertyDescription
--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.