# Timeline

> Dated events in an ordered list along a line.

Page: https://svelte-lean.vercel.app/primitives/timeline · Markdown: https://svelte-lean.vercel.app/primitives/timeline.md · Index: https://svelte-lean.vercel.app/llms.txt

Dated events in an ordered list drawn along a line, for an activity feed, a deployment history or an order’s progress. Tier 0.

```svelte
<script lang="ts">
	import '@svelte-lean/styles/timeline.css';
</script>

<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-color="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-color="error">
		<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>
```

Markup and CSS only; the dots and the line are generated content on each list item.

## Examples

### Oldest first

```svelte
<ol role="list" data-slean="timeline" aria-label="Order 1042">
	<li data-color="info">
		<time datetime="2026-09-24T10:02">24 Sep, 10:02</time>
		<p data-slean-part="title">Order placed</p>
	</li>
	<li data-color="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-color="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>
```

With the oldest event first, the line into the first pending event is dashed as well.

### Activity feed

```svelte
<script lang="ts">
	import '@svelte-lean/styles/button.css';

	type Event = { id: number; at: Date; title: string; color?: string; pending?: boolean };

	let events: Event[] = $state([
		{ id: 4, at: new Date('2027-01-15T09:00Z'), title: 'Release', pending: true },
		{ id: 3, at: new Date('2026-10-02T16:45Z'), title: 'Approved by Lin', color: 'success' },
		{ id: 2, at: new Date('2026-10-02T10:20Z'), title: 'Changes requested', color: 'warning' },
		{ id: 1, at: new Date('2026-10-01T09:05Z'), title: 'Pull request opened', color: 'info' }
	]);
	const format = new Intl.DateTimeFormat('en', {
		dateStyle: 'medium',
		timeStyle: 'short',
		timeZone: 'UTC'
	});

	function comment() {
		events.splice(1, 0, { id: events.length + 1, at: new Date(), title: 'Comment added' });
	}
</script>

<ol role="list" data-slean="timeline" aria-label="Pull request 218">
	{#each events as event (event.id)}
		<li data-color={event.color}>
			<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>
<button type="button" class="add" data-slean="button" data-variant="outlined" data-color="neutral" data-size="small"
	onclick={comment}>
	Add a comment
</button>

<style>
	.add {
		margin-top: 1rem;
	}
</style>
```

Rendered from data with Intl.DateTimeFormat; the button inserts an event.

## Usage

Plain markup with a stylesheet: install `@svelte-lean/styles`. No JavaScript runs.

```bash
npm install @svelte-lean/styles
```

`stylesheets`

```ts
import '@svelte-lean/styles/timeline.css';
```

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. `data-color="info"`, `"success"`, `"warning"` or `"error"` tints a dot; say the outcome in the title.

Put the `pending` part in every event that has not happened yet. 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-color="info\|success\|warning\|error" 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. |

## Keyboard

| Key | When | Result |
| --- | --- | --- |
| `Tab` | links inside events | Moves between them (native); the timeline is not focusable |

## Styling

`timeline.css` draws the dot and the line from each item’s pseudo-elements, the colors, 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` | `calc(var(--slean-spacing) * 5)` | padding-block-end |
| `--slean-border-strong` | `var(--slean-neutral-8)` | border, background |
| `--slean-space-1` | `calc(var(--slean-spacing) * 1)` | 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` | `calc(var(--slean-spacing) * 2)` | padding-inline |
| `--slean-radius-full` | `9999px` | border-radius |
| `--slean-color-main` | `var(--slean-primary-main)` | border-color, background |

State selectors the stylesheet targets, all from the platform or ARIA: `:last-child`.

Variant attributes: `data-color` (`neutral`).

## Under the hood

Runtime and size

- **Tier**: 0 · Native
- **Behavior JS**: 0 B brotli · 0 B gzip · `fixtures/results.json (native-only)` · [method](https://svelte-lean.vercel.app/performance/methodology.md)
- **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>`

Tier 0: no behavior module; the Vite plugin maps `timeline` to nothing and the page ships no Svelte Lean JavaScript for it.

Why this implementation

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

Markup usually loses the “not yet” state. Here a pending event contains visible text such as “Scheduled”; `timeline.css` reads that 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 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 colors, the hollow pending dot and dashed line
- the contract: one element that both says and shows that an event is pending
- documentation

Accessibility

- 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 colors are color only and never the only signal: the title says what happened.

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 and hydration

Static markup: the server renders the events, their dates and the pending text. Format the dates on the server in the reader’s language.

Nothing is attached; the timeline reads the same before and after hydration.

Compatibility

`:has()` is Baseline 2023, widely available. Without it a pending event looks like a past one, and its text is still read.

Tests and source

- `apps/playground/tests/primitives/display-extra.spec.ts` Playwright, with page JavaScript enabled and disabled: roles, names, keyboard, geometry, axe
- `packages/styles/tests` static checks of the stylesheet (layers, tokens, specificity, dark parity)

- `packages/primitives/src/timeline/contract.md` the contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behavior
- `packages/primitives/src/timeline/contract.ts` typed constants: name, tier, base, parts, options, events
- `packages/styles/css/timeline.css` the optional stylesheet
