Primitives Layout and display
Timeline
Dated events in an ordered list, drawn along a line. Each event has a machine-readable date; an event that has not happened yet says so in text, which the stylesheet also reads to draw it hollow. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
<ol>,<time datetime>,:has()- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<ol> of <li> events with <time datetime>
On this page
Example
-
Maintenance window
Scheduled -
Deployed to production
Release 0.4.0 by Ada
-
Deployment failed
Health check timed out; rolled back
-
Build started
<ol role="list" data-slean="timeline" aria-label="Deployments">
<li>
<time datetime="2026-10-02">2 Oct</time>
<p data-slean-part="title">Maintenance window</p>
<span data-slean-part="pending">Scheduled</span>
</li>
<li data-variant="success">
<time datetime="2026-09-28T09:12">28 Sep, 09:12</time>
<p data-slean-part="title">Deployed to production</p>
<p data-slean-part="description">Release 0.4.0 by Ada</p>
</li>
<li data-variant="danger">
<time datetime="2026-09-27T17:40">27 Sep, 17:40</time>
<p data-slean-part="title">Deployment failed</p>
<p data-slean-part="description">Health check timed out; rolled back</p>
</li>
<li>
<time datetime="2026-09-27T17:31">27 Sep, 17:31</time>
<p data-slean-part="title">Build started</p>
</li>
</ol><script lang="ts">
import '@svelte-lean/styles/timeline.css';
let { events } = $props();
const format = new Intl.DateTimeFormat('en', { dateStyle: 'medium', timeStyle: 'short' });
</script>
<ol role="list" data-slean="timeline" aria-label="Deployments">
{#each events as event (event.id)}
<li data-variant={event.tone}>
<time datetime={event.at.toISOString()}>{format.format(event.at)}</time>
<p data-slean-part="title">{event.title}</p>
{#if event.pending}<span data-slean-part="pending">Scheduled</span>{/if}
</li>
{/each}
</ol>Why this implementation exists
An activity feed, a deployment history or an order’s progress is a list of events in order. <ol> gives each event its position, and <time datetime> gives its date in a form software can read while the text shows it the way the reader expects.
The one state markup usually loses is “not yet”. Here a pending event contains visible text such as “Scheduled”; timeline.css reads that same element through :has() to draw a hollow dot and a dashed line, so what is seen and what is read come from one place. Tones on the other events tint the dots only; the text says what happened.
The browser owns
- the ordered list and each event’s position
- the machine-readable date in <time datetime>
- the pending text, read with the event
Svelte Lean owns
- timeline.css: the dots, the line, the tones, the hollow pending dot and dashed line
- the contract: one element that both says and shows that an event is pending
- documentation
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
npm install @svelte-lean/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@svelte-lean/styles/timeline.css';Render one <li> per event with a <time datetime>, an optional title and description, and role="list" on the <ol> so Safari keeps announcing it as a list.
Set data-variant="info", "success", "warning" or "danger" on an event to tint its dot, and say the outcome in its title.
Put the pending part in every event that has not happened yet, in the language of the page. No event is marked current: a timeline records events, it does not track a position in a task (that is Steps).
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <ol role="list" data-slean="timeline"> | – | yes | role="list" keeps the list announced in Safari. Named with aria-label when useful. |
| event | <li> with <time datetime> | – | yes | data-variant="info|success|warning|danger" tints the dot; the text says what happened. |
| title | <p> or <span> | title | no | What happened. |
| description | <p> or <span> | description | no | Details in the muted color. |
| pending | <span> with text | pending | yes | In every event that has not happened yet. Visible; its presence draws the hollow dot. |
Runtime profile
Tier 0: the timeline has no behavior module, the Vite plugin maps timeline to no module, and the page ships no Svelte Lean JavaScript for it.
Accessibility contract
- An
<ol>withrole="list": screen readers announce the list and each event’s position, in Safari as well. <time datetime>carries the exact date; its text is what the reader sees and hears.- The pending text is read with the event; the hollow dot is drawn from it.
- The tones are color only and never the only signal: the title says what happened.
Keyboard
| Key | When | Result |
|---|---|---|
| Tab | links inside events | Moves between them (native); the timeline is not focusable |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
<ol>, <time datetime> | Widely available | Not applicable |
:has() | Baseline 2023 | A pending event looks like a past one; its text is still read |
Without JavaScript
Fully functional: the list, its dates and its states are markup.
Server rendering
Static markup: the server renders the events, their dates and the pending text. Format the dates on the server in the reader’s language.
Before hydration
Nothing is attached; the timeline reads the same before and after hydration.
Styling
timeline.css draws the dot and the line from each item’s pseudo-elements, the tones, the time, title and description text, and the pending state. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-fg | var(--slean-neutral-12) | color |
--slean-text-sm | 0.875rem | font-size |
--slean-space-5 | 1.25rem | padding-block-end |
--slean-border-strong | var(--slean-neutral-8) | border, background |
--slean-space-1 | 0.25rem | inset-block-start, inset-block-end, margin-block-start |
--slean-border | var(--slean-neutral-6) | border-inline-start |
--slean-fg-muted | var(--slean-neutral-11) | color |
--slean-text-xs | 0.75rem | font-size |
--slean-font-weight-medium | 500 | font-weight |
--slean-space-2 | 0.5rem | padding-inline |
--slean-radius-full | 9999px | border-radius |
--slean-info | var(--slean-accent) | border-color, background |
--slean-success | oklch(55% 0.15 150) | border-color, background |
--slean-warning | oklch(76% 0.16 80) | border-color, background |
--slean-danger | oklch(55% 0.2 25) | border-color, background |
State selectors the stylesheet targets, all from the platform or ARIA: :last-child.
Variant attributes: data-variant (info, success, warning, danger).
Compatibility notes
:has() is Baseline 2023, widely available. Without it a pending event looks like a past one, and its text is still read.
Examples
Oldest first
The order is the page's choice; the list announces the positions either way. With the oldest
event first, the pending events come last, and the line into the first of them is dashed as
well. data-variant="info" and "warning" tint the other dots.
-
Order placed
-
Payment retried
The first attempt was declined; the second went through.
-
Shipped
-
Delivery
Expected
<ol role="list" data-slean="timeline" aria-label="Order 1042">
<li data-variant="info">
<time datetime="2026-09-24T10:02">24 Sep, 10:02</time>
<p data-slean-part="title">Order placed</p>
</li>
<li data-variant="warning">
<time datetime="2026-09-25T08:40">25 Sep, 08:40</time>
<p data-slean-part="title">Payment retried</p>
<p data-slean-part="description">The first attempt was declined; the second went through.</p>
</li>
<li data-variant="success">
<time datetime="2026-09-26T15:15">26 Sep, 15:15</time>
<p data-slean-part="title">Shipped</p>
</li>
<li>
<time datetime="2026-09-30">30 Sep</time>
<p data-slean-part="title">Delivery</p>
<span data-slean-part="pending">Expected</span>
</li>
</ol>Testing
apps/playground/tests/primitives/display-extra.spec.tsPlaywright, with page JavaScript enabled and disabled: roles, names, keyboard, geometry, axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/timeline/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/timeline/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/timeline.cssthe optional stylesheet