Timeline
Summary
A vertical sequence of events with semantic marker dots and a
connecting rail: an ordered list (<ol>: the
order is meaningful) of items, each a
.soma-timeline-content block with a
-title (the time goes in a nested
<small>) and body paragraphs. Marker variants
-primary/-success/-warning/-danger
color the dot; the default is neutral. The title text should
state what happened; the marker color only echoes it.
When to use
| Case | Use it for |
|---|---|
| Audit trails | Who did what, when — curated and readable. |
| Session replay / post-mortems | Reconstructing what-happened-when as annotated milestones. |
| Activity feeds | Recent activity on a resource or project. |
| Log viewer instead | Raw high-volume event lines. The timeline is for curated milestones, not tails. |
Examples
HTML
One item, fully formed. The marker dot and rail are CSS
pseudo-elements on the <li>; the only markup
you author is the content block:
<ol class="soma-timeline">
<li class="soma-timeline-item soma-timeline-success">
<div class="soma-timeline-content">
<p class="soma-timeline-title">api-backend deployed <small>10:32</small></p>
<p>v2.3.1 to production; health checks green.</p>
</div>
</li>
</ol>
Status variants are one class on the item; omit it for the neutral default:
<li class="soma-timeline-item">…</li> <!-- neutral -->
<li class="soma-timeline-item soma-timeline-primary">…</li> <!-- in progress -->
<li class="soma-timeline-item soma-timeline-success">…</li> <!-- completed -->
<li class="soma-timeline-item soma-timeline-warning">…</li> <!-- degraded -->
<li class="soma-timeline-item soma-timeline-danger">…</li> <!-- failed -->
Timestamps are a nested <small> in the
title. Leave it out for phase sequences where the order
matters but wall-clock times don't. Body paragraphs are
optional too; a bare title is a valid milestone:
<li class="soma-timeline-item soma-timeline-primary">
<div class="soma-timeline-content">
<p class="soma-timeline-title">Implementation</p>
</div>
</li>
Item anatomy
Each <li> draws two pseudo-elements: a 9px
marker dot (::before) aligned with the title line,
and a 1px rail (::after) running from below the dot
to the bottom of the item, where the next item's dot picks up.
The last item pads to zero and drops its rail, so the sequence
visibly ends. Because both are pseudo-elements positioned with
logical properties, there is nothing to author or clean up per
item, and the whole component mirrors automatically under
dir="rtl" (content indents from the other edge,
dots and rail flip with it).
The content block is ordinary flow: the title paragraph, then
any number of body paragraphs. Anything that fits prose fits
here (links, <code>, a
badge), but keep each item a
milestone, not an article; long narratives belong on the page
the title links to. The time <small> is
subtle-tinted (decorative by design): when the exact time is
load-bearing information rather than context, put it in the
body text too.
Use <ol>, not <ul> — the
order is the meaning, and assistive technology announces
position ("2 of 5") for ordered lists. Pick one direction per
view (newest-first for feeds, oldest-first for post-mortems)
and keep it stable.
CSS classes
| Class | Effect |
|---|---|
.soma-timeline | The list (<ol>); draws nothing itself. |
.soma-timeline-item | One event: a neutral marker dot plus the rail down to the next item (the last item drops the rail). |
-primary / -success / -warning / -danger | On the item: semantic marker color (in progress / completed / degraded / failed by convention; your copy decides). |
.soma-timeline-content | The text block beside the marker. |
.soma-timeline-title | Event title; a nested <small> carries the time (optional, subtle-tinted). |
JavaScript
None: the timeline is CSS-only; assembling and ordering the
items is the consumer's concern. For a live feed, build the
item and insert it at the end that matches the view's order.
The rail and end-of-sequence styling re-resolve on their own
(both are plain CSS on :last-child):
const STATUS_CLASS = {
progress: 'soma-timeline-primary',
ok: 'soma-timeline-success',
degraded: 'soma-timeline-warning',
failed: 'soma-timeline-danger',
};
function timelineItem({ title, time, body, status }) {
const li = document.createElement('li');
li.className = 'soma-timeline-item';
if (STATUS_CLASS[status]) li.classList.add(STATUS_CLASS[status]);
const content = document.createElement('div');
content.className = 'soma-timeline-content';
const titleEl = document.createElement('p');
titleEl.className = 'soma-timeline-title';
titleEl.textContent = title + ' '; // event data stays text
if (time) {
const small = document.createElement('small');
small.textContent = time;
titleEl.appendChild(small);
}
content.appendChild(titleEl);
if (body) {
const p = document.createElement('p');
p.textContent = body;
content.appendChild(p);
}
li.appendChild(content);
return li;
}
// Newest-first feed → prepend; oldest-first post-mortem → append.
const feed = document.querySelector('#activity');
feed.prepend(timelineItem({
title: 'Deploy completed', time: '10:32',
body: 'Health checks green.', status: 'ok',
}));