Theming
No colour is in the markup. Items carry classes, the classes read CSS custom properties (--tl-*), and the stylesheet that maps them is generated with the frame. Restyle the whole timeline with CSS alone; dark mode is not a second render.
Live example: Restyled with CSS alone
Loading…
Run “Restyled with CSS alone” on your own machine
Make an empty folder and put this example’s two files in it:
index.html— the page the code runs in, its markup and styles:<style> /* The tokens are read on the timeline's root: set them there, or on one item. Pale fills carry dark text, so they belong to the light theme only. */ #timeline .tl[data-tl-theme="light"] { --tl-item-fill: #fde7f3; --tl-item-stroke: #a3195b; --tl-accent: #a3195b; --tl-focus: #a3195b; } #timeline .tl[data-tl-theme="light"] .urgent { --tl-item-fill: #ffd6a5; } </style> <div style="padding: 6px 10px; border-bottom: 1px solid #d5d9de"> <button type="button" id="theme">Switch to dark</button> </div> <div id="timeline" style="height: 250px"></div>main.js— the code, as it is in the editor now, your edits included:In a terminal in that folder, install the library and Vite (it lets the browser follow the
imports), then start it:npm init -y npm install @temporeum/timeline-element npm install --save-dev vite npx viteOpen the address it prints,
http://localhost:5173/.
#timeline .tl[data-tl-theme="light"] {
--tl-item-fill: #fde7f3;
--tl-item-stroke: #a3195b;
--tl-accent: #a3195b;
--tl-focus: #a3195b;
}
#timeline .tl[data-tl-theme="light"] .urgent { --tl-item-fill: #ffd6a5; }From the running example example/theming
The tokens
| Token | Paints |
|---|---|
--tl-surface, --tl-surface-alt | the background, and banding and pinned lanes |
--tl-text, --tl-text-muted | labels outside items, and tick labels |
--tl-line, --tl-line-strong | ticks, minor and major |
--tl-item-fill, --tl-item-stroke, --tl-item-text | items and the labels drawn on them |
--tl-accent | clusters and loading dashes |
--tl-now, --tl-marker, --tl-era, --tl-era-text | decorations |
--tl-focus | the focus ring and the selection |
The tokens are declared on the timeline's root element (.tl), so set yours there, with a selector at least as specific — #timeline .tl — or on a single item through its className: the item's own declaration is the one its fill reads.
Light, dark, auto
theme: "auto" (the default) follows prefers-color-scheme; "light" and "dark" pin it, which is what a page with its own theme switch needs. Changing it with update({ theme }) swaps the tokens; the items are the same nodes. Both built-in themes pass WCAG AA contrast for every text pair; a custom fill has to carry the text drawn on it — which is why the example scopes its pale fills to the light theme. A theme of your own is an object, { name, tokens }, with every token named (LIGHT_THEME and DARK_THEME from @temporeum/timeline-core are two), and it is type-checked. Each token is a colour — no ;, brace, angle bracket, backslash, * or url( — or resolveTheme throws a RangeError: the tokens go into a stylesheet, and a token is never a way to write a rule.
Sizes and type
Colours are CSS; sizes are configuration. The layout packs rows of a height and places labels it measured in a type size and a font, so a size set in a stylesheet would draw text into room measured for other text. metrics sets them over the density's:
"metrics": { "rowSize": 36, "fontSize": 14, "lineHeight": 18, "cornerRadius": 10, "fontFamily": "Georgia, 'Times New Roman', serif" }From the running example example/theming-element
| Key | Sets |
|---|---|
rowSize, rowGap, laneGap | a row's height (a column's width, vertical), and the gaps between rows and lanes |
fontSize, lineHeight, labelPadding | the items' labels |
tickFontSize, axisSize | the axis' labels, and the band they are in |
cornerRadius | the items' corners |
fontFamily | the font stack everything is drawn and measured in |
A function sets each level apart, or a row and a column: metrics: (base) => ({ rowSize: base.rowSize + 8 }) gets the density's own sizes. What density: "auto" decides with stays the level's, and so does the smallest drawn item. A web font has to be loaded before the timeline measures with it (await document.fonts.ready); a size nothing can be drawn at, or a font stack that is not one, throws where it is configured. timeline.metrics says what the last frame was drawn with, for a panel lined up beside it. Under the element, the engine takes the sizes as a function of the level (RenderOptions.metrics, densityMetrics by default).
Inside the custom element
<temporeum-timeline> draws in its shadow root, where a page's selectors do not reach. The page reaches it two ways:
- The tokens, set on the element. Inside the shadow root they are declared on
:host, and a page's rule for the element wins over those whatever its specificity:temporeum-timeline { --tl-item-fill: … }, or a class on one element. ::part(), for any node. Every node the timeline draws carries its classes as its parts, read without thetl-prefix: an item isitem, a selected oneitem item-selected, the roottimeline, a major tick's labeltick-label tick-label-major, a group headergroup. A class of your own — an item'sclassName— is a part as it is:temporeum-timeline::part(urgent). Atl-class given in data is left out, on items, groups, decorations and arrows alike: those classes are the timeline's, and one of them on your node would draw it as selected or have it taken for a control.
Live example: The element styled from the page
Loading…
Run “The element styled from the page” on your own machine
Make an empty folder and put this example’s two files in it:
index.html— the page the code runs in, its markup and styles:<style> /* The element draws in a shadow root. The page reaches it two ways: the tokens it sets on the element, and ::part() for any node of the drawing. */ temporeum-timeline { --tl-item-fill: #e6f4ea; --tl-item-stroke: #1e7b45; --tl-item-text: #10331f; --tl-focus: #1e7b45; } temporeum-timeline::part(urgent) { fill: #ffe1c2; stroke: #a14a00; } temporeum-timeline::part(item-selected) { stroke-width: 3; } temporeum-timeline::part(tick-label-major) { font-weight: 600; } </style> <temporeum-timeline height="300" theme="light" aria-label="Editorial calendar"> <script type="application/json"> { "items": [ { "id": "a", "start": "2026-06-01", "end": "2026-06-06", "label": "Draft" }, { "id": "b", "start": "2026-06-05", "end": "2026-06-10", "label": "Legal review", "className": "urgent" }, { "id": "c", "start": "2026-06-07", "end": "2026-06-13", "label": "Translate" }, { "id": "d", "start": "2026-06-14", "end": "2026-06-16", "label": "Publish" } ], "metrics": { "rowSize": 36, "fontSize": 14, "lineHeight": 18, "cornerRadius": 10, "fontFamily": "Georgia, 'Times New Roman', serif" } } </script> </temporeum-timeline>main.js— the code, as it is in the editor now, your edits included:In a terminal in that folder, install the library and Vite (it lets the browser follow the
imports), then start it:npm init -y npm install npm install --save-dev vite npx viteOpen the address it prints,
http://localhost:5173/.
temporeum-timeline {
--tl-item-fill: #e6f4ea;
--tl-item-stroke: #1e7b45;
}
temporeum-timeline::part(urgent) {
fill: #ffe1c2;
stroke: #a14a00;
}From the running example example/theming-element
A server-rendered element (renderTimelineHtml) carries the same parts and tokens, so the page's rules apply before the script runs. An exported SVG or PNG is drawn with the theme's own stylesheet: the page's rules are not in the file. Outside a shadow root nothing has a part — the page's stylesheet reaches the classes themselves.
Your own content in items
A label is one line of text. For more, there are two ways, both inside the item's box, both leaving the item's label as its accessible name:
template, HTML a function returns, sanitised (/items; see item kinds and templates). It is drawn on the server too.slots, a box a framework renders into: the React component'srenderItem, the Vue component's#itemslot (frameworks). Each drawn item gets a host element of its own, the same element for as long as the item is drawn — through pans, zooms and live data — andslotsChangedsays when the items drawn are others, in the frame that drew them. Only the items on screen have one: ten thousand items render the few hundred in view.
Live example: Your own content in every item
Loading…
Run “Your own content in every item” on your own machine
Make an empty folder and put this example’s two files in it:
index.html— the page the code runs in, its markup and styles:<style> .card { display: flex; gap: 6px; align-items: center; height: 100%; padding: 0 6px; font-size: 12px; } .card progress { width: 48px; height: 6px; } </style> <div id="timeline" style="height: 280px"></div>main.js— the code, as it is in the editor now, your edits included:In a terminal in that folder, install the library and Vite (it lets the browser follow the
imports), then start it:npm init -y npm install @temporeum/timeline-element npm install --save-dev vite npx viteOpen the address it prints,
http://localhost:5173/.
const timeline = createTimeline(document.querySelector("#timeline"), {
items,
slots: true,
ariaLabel: "Product sprint",
});
timeline.on("slotsChanged", ({ slots }) => render(slots));
render(timeline.slots);From the running example example/item-slots
The content is a picture of the item: the pointer and the keyboard act on the item itself, so a drag, a click and a key go where they do without it. Both need the /items plugin.