Timeline
A vertical run of events, each with a dot, joined by a line. It suits anything with a real order and a real time: an audit trail, a delivery's progress, the history of a ticket.
When to use it
When the sequence is the point. A timeline says "this, then this, then this" more directly than a list, and it does it in a shape people already read as chronological.
When the sequence is a process the reader is currently moving through — checkout, an onboarding wizard — that is a stepper, which is horizontal, numbered, and about what happens next rather than what already happened.
-
Order placed
— payment authorised
-
Dispatched
— left the Bristol depot
-
Out for delivery
Expected
<ol class="timeline">
<li class="timeline-item is-done">
<span class="timeline-dot">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg>
</span>
<div class="stack stack-1">
<strong>Order placed</strong>
<p class="text-muted"><time datetime="2026-09-04T09:12">4 September, 09:12</time> — payment authorised</p>
</div>
</li>
<li class="timeline-item is-done">
<span class="timeline-dot">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg>
</span>
<div class="stack stack-1">
<strong>Dispatched</strong>
<p class="text-muted"><time datetime="2026-09-05T16:40">5 September, 16:40</time> — left the Bristol depot</p>
</div>
</li>
<li class="timeline-item">
<span class="timeline-dot"></span>
<div class="stack stack-1">
<strong>Out for delivery</strong>
<p class="text-muted">Expected <time datetime="2026-09-06">6 September</time></p>
</div>
</li>
</ol>
<ol class="timeline">
<li class="timeline-item is-done">
<span class="timeline-dot">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg>
</span>
<div class="stack stack-1">
<strong>Order placed</strong>
<p class="text-muted"><time datetime="2026-09-04T09:12">4 September, 09:12</time> — payment authorised</p>
</div>
</li>
<li class="timeline-item is-done">
<span class="timeline-dot">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg>
</span>
<div class="stack stack-1">
<strong>Dispatched</strong>
<p class="text-muted"><time datetime="2026-09-05T16:40">5 September, 16:40</time> — left the Bristol depot</p>
</div>
</li>
<li class="timeline-item">
<span class="timeline-dot"></span>
<div class="stack stack-1">
<strong>Out for delivery</strong>
<p class="text-muted">Expected <time datetime="2026-09-06">6 September</time></p>
</div>
</li>
</ol>
<ol> with a class already has its markers and padding removed by
src/02-reset.css, so no list reset is needed. The
<time datetime> is not Deck's doing and is worth the keystrokes —
it is what makes the date machine-readable.
How it is drawn
.timeline-item is a two-column grid: a fixed 22px track for the dot and
everything else for the content. That fixed track is what keeps the dots in a straight
line however tall or short the entries are.
The connector is a pseudo-element on the item, not a border on the column:
.timeline-item:not(:last-child)::before {
content: "";
position: absolute;
inset-block: 22px 0;
inset-inline-start: 10px;
inline-size: 2px;
background: var(--line);
}
Two things follow. It starts at 22px — below the dot — so the line runs
between dots rather than through them. And :not(:last-child) means the
final entry has no trailing line, which is what stops the timeline looking like it
continues past the last thing that happened.
Both numbers are hard-coded: the 22px track, the 22px offset and the 10px inset are written out rather than derived from a custom property. Resizing the dot means changing four values in three rules, so it is not a one-line override — see Overriding it.
Completed entries
.is-done on the item — not the dot — fills its dot with the brand
colour and sets the icon inside it to the on-brand text colour. Everything else about
the entry is unchanged.
-
Done Complete
-
Not done Pending
<ol class="timeline">
<li class="timeline-item is-done">
<span class="timeline-dot">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg>
</span>
<div><strong>Done</strong> <span class="badge badge-good">Complete</span></div>
</li>
<li class="timeline-item">
<span class="timeline-dot"></span>
<div><strong>Not done</strong> <span class="badge">Pending</span></div>
</li>
</ol>
<ol class="timeline">
<li class="timeline-item is-done">
<span class="timeline-dot">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg>
</span>
<div><strong>Done</strong> <span class="badge badge-good">Complete</span></div>
</li>
<li class="timeline-item">
<span class="timeline-dot"></span>
<div><strong>Not done</strong> <span class="badge">Pending</span></div>
</li>
</ol>
The dot is a plain <span> with a background — it says nothing. Note
what the example does: the state is also written as a
badge, because that is the version a screen reader and a
monochrome printer can both read.
Classes
Generated from src/07-components.css by tools/docs/extract.mjs.
| Class | Layer | What it does | Source |
|---|---|---|---|
.timeline |
deck.components |
Sets display, flex-direction. | 07-components.css:781 |
.timeline-dot |
deck.components |
Sets inline-size, block-size, border-radius, background, and 4 more. | 07-components.css:797 |
.timeline-item |
deck.components |
Sets display, grid-template-columns, gap, padding-block-end, and 1 more. | 07-components.css:782 |
.is-done |
deck.components |
No declarations. | 07-components.css:807 |
Tokens
| Token | Live | Declared value |
|---|---|---|
--line |
light-dark(var(--ink-200), oklch(28% .016 var(--hue-neutral))) |
|
--line-strong |
light-dark(var(--ink-300), oklch(36% .018 var(--hue-neutral))) |
|
--surface |
light-dark(var(--ink-0), var(--ink-950)) |
|
--brand-600 |
oklch(51.6% var(--chroma-brand) var(--hue-brand)) |
|
--text-on-brand |
oklch(99% .01 var(--hue-brand)) |
|
--r-full |
999px |
|
--space-3 |
.75rem |
|
--space-5 |
1.25rem |
Accessibility
-
Use an
<ol>. The order is the information. An ordered list is announced with its position — "3 of 5" — which is exactly what the shape is conveying visually. - The dot and the line are decoration. One is an empty element with a background, the other a pseudo-element. Neither is announced, and neither should be: the content of the entry has to stand on its own when read aloud.
-
.is-doneis colour alone. A filled dot and an empty dot differ only in fill. Put the state in the entry — a badge, or simply the word — as the example does. -
An icon inside the dot needs hiding. A tick in a
.timeline-dotis repeating what the text should already say. Give itaria-hidden="true", or it announces alongside the entry as a second, contentless item. -
Give each entry a real date.
<time datetime>rather than "2 days ago" alone. Relative times are friendly and ambiguous, and they go stale on a printed or cached page. - A long timeline is a long list. Twenty entries is twenty things to pass through. Put a heading above it so it can be skipped, and consider paginating or collapsing older entries.
Right to left
The dot track moves to the right and the connector follows, because the item is a grid
and the line is placed with inset-inline-start. No rule in
src/19-logical.css is involved — the component was written with logical
properties from the start, so it mirrors on its own.
-
تم استلام الطلب
-
قيد التوصيل
<ol dir="rtl" class="timeline">
<li class="timeline-item is-done">
<span class="timeline-dot">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg>
</span>
<div><strong>تم استلام الطلب</strong></div>
</li>
<li class="timeline-item">
<span class="timeline-dot"></span>
<div><strong>قيد التوصيل</strong></div>
</li>
</ol>
<ol dir="rtl" class="timeline">
<li class="timeline-item is-done">
<span class="timeline-dot">
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg>
</span>
<div><strong>تم استلام الطلب</strong></div>
</li>
<li class="timeline-item">
<span class="timeline-dot"></span>
<div><strong>قيد التوصيل</strong></div>
</li>
</ol>
Reduced motion
Nothing on the timeline animates — no transitions, no keyframes, no transforms. There is
nothing for prefers-reduced-motion to act on, and no guard is needed.
If you animate entries appearing as they load, that motion is yours and needs its own guard.
Printing
.timeline-item is one of the few things src/99-print.css
protects with break-inside: avoid, alongside paragraphs, list items and
.stat. An event will not be split across a page boundary.
The dots and the connector are backgrounds, and are not in the list of elements given
print-color-adjust: exact — so both are dropped and the timeline prints as
an indented list of events with a gap on the left where the track was. The content
survives; the shape does not, and neither does the completed state.
Which is the same argument as everywhere else on this page: if
.is-done matters, it has to be a word.
Overriding it
@layer app.components {
/* A larger dot. Four values have to move together. */
.timeline-item { grid-template-columns: 32px 1fr; }
.timeline-dot { inline-size: 32px; block-size: 32px; }
.timeline-item:not(:last-child)::before {
inset-block-start: 32px; /* clear the dot */
inset-inline-start: 15px; /* half the dot, minus half the 2px line */
}
/* A dashed connector for a projected future */
.timeline-item.is-planned::before {
background: repeating-linear-gradient(
to bottom, var(--line) 0 4px, transparent 4px 8px);
}
}
The line's inset-inline-start is half the dot width minus half the line
width. Get it wrong by a pixel and the connector visibly misses the dot, which is the
main hazard in resizing this component.
When not to use it
- Not for a process in progress. Checkout, onboarding and any "you are here" sequence is a stepper.
- Not for unordered items. If the order does not matter, the line is claiming something untrue. Use a list.
- Not for data you would rather compare. Events with durations and overlaps are a chart, not a column. Deck's charts are the nearer tool.
- Not for hundreds of entries. The connector implies you can see the whole sequence. Past a screenful or two that stops being true and the shape stops paying for itself.
-
Not with
.is-doneas the only signal. It is a colour, and it is gone on paper.