sveltelean Primitives
Versionv0.2.0 GitHub

Example

Deployments, newest first
  1. Maintenance window

    Scheduled
  2. Deployed to production

    Release 0.4.0 by Ada

  3. Deployment failed

    Health check timed out; rolled back

  4. 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>
Tier 0: the dots and the line are generated content on each list item. The tones tint the dots; the titles say what happened. Everything here works with page JavaScript disabled.

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/styles
stylesheets
import '@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

PartElementdata-slean-partRequiredNotes
root<ol role="list" data-slean="timeline">–yesrole="list" keeps the list announced in Safari. Named with aria-label when useful.
event<li> with <time datetime>–yesdata-variant="info|success|warning|danger" tints the dot; the text says what happened.
title<p> or <span>titlenoWhat happened.
description<p> or <span>descriptionnoDetails in the muted color.
pending<span> with textpendingyesIn 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> with role="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

KeyWhenResult
Tablinks inside eventsMoves between them (native); the timeline is not focusable

Platform features

FeatureBaselineOutside the target
<ol>, <time datetime>Widely availableNot applicable
:has()Baseline 2023A 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:

TokenDefault (light)Applies to
--slean-fgvar(--slean-neutral-12)color
--slean-text-sm0.875remfont-size
--slean-space-51.25rempadding-block-end
--slean-border-strongvar(--slean-neutral-8)border, background
--slean-space-10.25reminset-block-start, inset-block-end, margin-block-start
--slean-bordervar(--slean-neutral-6)border-inline-start
--slean-fg-mutedvar(--slean-neutral-11)color
--slean-text-xs0.75remfont-size
--slean-font-weight-medium500font-weight
--slean-space-20.5rempadding-inline
--slean-radius-full9999pxborder-radius
--slean-infovar(--slean-accent)border-color, background
--slean-successoklch(55% 0.15 150)border-color, background
--slean-warningoklch(76% 0.16 80)border-color, background
--slean-dangeroklch(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.

  1. Order placed

  2. Payment retried

    The first attempt was declined; the second went through.

  3. Shipped

  4. Delivery

    Expected
oldest first
<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

Source