Temporeum Timelinev1.0.2

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

  2. 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 vite
  3. Open 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

TokenPaints
--tl-surface, --tl-surface-altthe background, and banding and pinned lanes
--tl-text, --tl-text-mutedlabels outside items, and tick labels
--tl-line, --tl-line-strongticks, minor and major
--tl-item-fill, --tl-item-stroke, --tl-item-textitems and the labels drawn on them
--tl-accentclusters and loading dashes
--tl-now, --tl-marker, --tl-era, --tl-era-textdecorations
--tl-focusthe 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

KeySets
rowSize, rowGap, laneGapa row's height (a column's width, vertical), and the gaps between rows and lanes
fontSize, lineHeight, labelPaddingthe items' labels
tickFontSize, axisSizethe axis' labels, and the band they are in
cornerRadiusthe items' corners
fontFamilythe 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 the tl- prefix: an item is item, a selected one item item-selected, the root timeline, a major tick's label tick-label tick-label-major, a group header group. A class of your own — an item's className — is a part as it is: temporeum-timeline::part(urgent). A tl- 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
  1. 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:

  2. 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 vite
  3. Open 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's renderItem, the Vue component's #item slot (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 — and slotsChanged says 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
  1. 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:

  2. 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 vite
  3. Open 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.