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

CaseUse it for
Audit trailsWho did what, when — curated and readable.
Session replay / post-mortemsReconstructing what-happened-when as annotated milestones.
Activity feedsRecent activity on a resource or project.
Log viewer insteadRaw high-volume event lines. The timeline is for curated milestones, not tails.

Examples

Basic items

Neutral markers, title + body:

  1. Project created Mon 09:14

    tomas created "api-backend" from the service template.

  2. Settings changed Mon 11:40

    Region pinned to us-east.

  3. Member added Tue 08:02

    jana joined as maintainer.

The default marker is neutral; the rail connects items and drops after the last one.

Per-status markers

Default · -primary · -success · -warning · -danger:

  1. Neutral (default) 09:00

  2. Primary — in progress 09:05

  3. Success — completed 09:10

  4. Warning — degraded 09:15

  5. Danger — failed 09:20

The status class goes on the item; the title words carry the meaning, the dot color only echoes it.

Without timestamps

The small time is optional:

  1. Proposal accepted

    Scope agreed with the platform team.

  2. Implementation

    In progress — tracked in NW-214.

  3. Rollout

    Pending sign-off.

For phase or roadmap sequences where the order matters but wall-clock times don't, just leave out the nested <small>.

Deploy sequence

Timestamps + status markers composed:

  1. Deploy started 09:58

    api-backend v2.3.1.

  2. Image pull failed 09:58

    Registry timeout; retrying.

  3. Deploy completed 10:32

    Health checks green.

The real-world shape: times in the titles, one status class per milestone.

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

ClassEffect
.soma-timelineThe list (<ol>); draws nothing itself.
.soma-timeline-itemOne event: a neutral marker dot plus the rail down to the next item (the last item drops the rail).
-primary / -success / -warning / -dangerOn the item: semantic marker color (in progress / completed / degraded / failed by convention; your copy decides).
.soma-timeline-contentThe text block beside the marker.
.soma-timeline-titleEvent 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',
}));