32 CSS Timelines27 / 32

Pure CSSMIT licensed

SaaS Changelog Timeline — Version Badges with Sticky Month Rail

The docs-site staple: releases on a left rail with semver badges, category chips (New / Improved / Fixed) double-coded with glyphs, and month labels that stick in the gutter as their releases scroll by — the layout Linear, Stripe and every serious devtool converged on for 'What's new' pages.

Published Updated

Live Demo
Try it

The code

<section class="tls-27" aria-label="Changelog timeline demo">
  <div class="tls-27__inner">
    <h3 class="tls-27__title">What's new</h3>
    <div class="tls-27__month">
      <p class="tls-27__mlabel">Jul<br>2026</p>
      <div class="tls-27__entries">
        <article class="tls-27__rel tls-27__rel--major">
          <header class="tls-27__rhead"><code class="tls-27__ver">v3.0.0</code><time datetime="2026-07-14">Jul 14</time></header>
          <h4 class="tls-27__rtitle">The big one: workspaces</h4>
          <ul class="tls-27__list" role="list">
            <li><span class="tls-27__kind" data-kind="new">New</span>Multi-workspace accounts with per-workspace billing</li>
            <li><span class="tls-27__kind" data-kind="new">New</span>Granular roles: admin, editor, commenter, guest</li>
            <li><span class="tls-27__kind" data-kind="improved">Improved</span>Search is 6× faster on large orgs</li>
          </ul>
        </article>
        <article class="tls-27__rel">
          <header class="tls-27__rhead"><code class="tls-27__ver">v2.14.1</code><time datetime="2026-07-03">Jul 3</time></header>
          <ul class="tls-27__list" role="list">
            <li><span class="tls-27__kind" data-kind="fixed">Fixed</span>Exported CSVs no longer double-escape quotes</li>
            <li><span class="tls-27__kind" data-kind="fixed">Fixed</span>Safari: date picker closes on first tap again</li>
          </ul>
        </article>
      </div>
    </div>
    <div class="tls-27__month">
      <p class="tls-27__mlabel">Jun<br>2026</p>
      <div class="tls-27__entries">
        <article class="tls-27__rel">
          <header class="tls-27__rhead"><code class="tls-27__ver">v2.14.0</code><time datetime="2026-06-19">Jun 19</time></header>
          <h4 class="tls-27__rtitle">Automations, round two</h4>
          <ul class="tls-27__list" role="list">
            <li><span class="tls-27__kind" data-kind="new">New</span>Scheduled triggers (cron syntax supported)</li>
            <li><span class="tls-27__kind" data-kind="improved">Improved</span>Run history keeps 90 days on all plans</li>
            <li><span class="tls-27__kind" data-kind="fixed">Fixed</span>Webhook retries respect Retry-After headers</li>
          </ul>
        </article>
        <article class="tls-27__rel">
          <header class="tls-27__rhead"><code class="tls-27__ver">v2.13.2</code><time datetime="2026-06-05">Jun 5</time></header>
          <ul class="tls-27__list" role="list">
            <li><span class="tls-27__kind" data-kind="improved">Improved</span>Keyboard nav: J/K move between rows everywhere</li>
          </ul>
        </article>
      </div>
    </div>
  </div>
</section>
.tls-27,
.tls-27 *,
.tls-27 *::before,
.tls-27 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tls-27 {
  min-height: 100vh;
  width: 100%;
  --tl-accent: #4f46e5;
  --tl-line: #e5e7eb;
  --tl-bg: #ffffff;
  --tl-ink: #111827;
  --tl-mut: #6b7280;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: var(--tl-bg);
  padding: 64px 24px;
  display: grid;
  place-items: center;
}

.tls-27__inner {
  width: min(620px,100%);
}

.tls-27__title {
  font-size: clamp(22px,3.5vw,28px);
  font-weight: 800;
  color: var(--tl-ink);
  letter-spacing: -.02em;
  margin-bottom: 32px;
}

.tls-27__month {
  display: grid;
  grid-template-columns: 64px 1fr;
  gap: 20px;
}

.tls-27__mlabel {
  position: sticky;
  top: 24px;
  align-self: start;
  font-size: 13px;
  font-weight: 800;
  line-height: 1.3;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--tl-mut);
  text-align: right;
  padding-top: 2px;
}

.tls-27__entries {
  position: relative;
  padding: 0 0 36px 26px;
  border-left: 2px solid var(--tl-line);
}

.tls-27__rel {
  position: relative;
  padding-bottom: 28px;
}

.tls-27__rel:last-child {
  padding-bottom: 0;
}

.tls-27__rel::before {
  content: '';
  position: absolute;
  left: -33px;
  top: 5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--tl-bg);
  border: 3px solid var(--tl-accent);
  box-shadow: 0 0 0 4px var(--tl-bg);
}

.tls-27__rel--major::before {
  left: -35px;
  width: 14px;
  height: 14px;
  background: var(--tl-accent);
}

.tls-27__rhead {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 8px;
}

.tls-27__ver {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 13px;
  font-weight: 700;
  color: var(--tl-accent);
  background: #eef2ff;
  border-radius: 6px;
  padding: 2px 8px;
}

.tls-27__rel--major .tls-27__ver {
  color: #fff;
  background: var(--tl-accent);
}

.tls-27__rhead time {
  font-size: 12.5px;
  color: var(--tl-mut);
}

.tls-27__rtitle {
  font-size: 16.5px;
  font-weight: 750;
  color: var(--tl-ink);
  letter-spacing: -.01em;
  margin-bottom: 10px;
}

.tls-27__list {
  list-style: none;
  display: grid;
  gap: 8px;
}

.tls-27__list li {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 14px;
  line-height: 1.55;
  color: #374151;
  text-wrap: pretty;
}

.tls-27__kind {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .04em;
  border-radius: 999px;
  padding: 2px 9px;
  min-width: 64px;
  justify-content: center;
}

.tls-27__kind::before {
  font-weight: 800;
}

.tls-27__kind[data-kind="new"] {
  color: #166534;
  background: #f0fdf4;
}

.tls-27__kind[data-kind="new"]::before {
  content: '+';
}

.tls-27__kind[data-kind="improved"] {
  color: #1d4ed8;
  background: #eff6ff;
}

.tls-27__kind[data-kind="improved"]::before {
  content: '↑';
}

.tls-27__kind[data-kind="fixed"] {
  color: #9a3412;
  background: #fff7ed;
}

.tls-27__kind[data-kind="fixed"]::before {
  content: '✓';
}

@media (max-width: 520px) {
  .tls-27__month {
    grid-template-columns: 1fr;
  }

  .tls-27__mlabel {
    position: static;
    text-align: left;
    padding-bottom: 10px;
  }

  .tls-27__mlabel br {
    display: none;
  }
}
/* No JavaScript — month labels are position:sticky inside their month group; category chips are data-kind attributes rendering glyph + text (never color alone). */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Timeline from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: SaaS Changelog Timeline — Version Badges with Sticky Month Rail
Source: https://codefronts.com/layouts/css-timelines/ticker-tape-changelog/

The docs-site staple: releases on a left rail with semver badges, category chips (New / Improved / Fixed) double-coded with glyphs, and month labels that stick in the gutter as their releases scroll by — the layout Linear, Stripe and every serious devtool converged on for 'What's new' pages.
## HTML
```html
<section class="tls-27" aria-label="Changelog timeline demo">
  <div class="tls-27__inner">
    <h3 class="tls-27__title">What's new</h3>
    <div class="tls-27__month">
      <p class="tls-27__mlabel">Jul<br>2026</p>
      <div class="tls-27__entries">
        <article class="tls-27__rel tls-27__rel--major">
          <header class="tls-27__rhead"><code class="tls-27__ver">v3.0.0</code><time datetime="2026-07-14">Jul 14</time></header>
          <h4 class="tls-27__rtitle">The big one: workspaces</h4>
          <ul class="tls-27__list" role="list">
            <li><span class="tls-27__kind" data-kind="new">New</span>Multi-workspace accounts with per-workspace billing</li>
            <li><span class="tls-27__kind" data-kind="new">New</span>Granular roles: admin, editor, commenter, guest</li>
            <li><span class="tls-27__kind" data-kind="improved">Improved</span>Search is 6× faster on large orgs</li>
          </ul>
        </article>
        <article class="tls-27__rel">
          <header class="tls-27__rhead"><code class="tls-27__ver">v2.14.1</code><time datetime="2026-07-03">Jul 3</time></header>
          <ul class="tls-27__list" role="list">
            <li><span class="tls-27__kind" data-kind="fixed">Fixed</span>Exported CSVs no longer double-escape quotes</li>
            <li><span class="tls-27__kind" data-kind="fixed">Fixed</span>Safari: date picker closes on first tap again</li>
          </ul>
        </article>
      </div>
    </div>
    <div class="tls-27__month">
      <p class="tls-27__mlabel">Jun<br>2026</p>
      <div class="tls-27__entries">
        <article class="tls-27__rel">
          <header class="tls-27__rhead"><code class="tls-27__ver">v2.14.0</code><time datetime="2026-06-19">Jun 19</time></header>
          <h4 class="tls-27__rtitle">Automations, round two</h4>
          <ul class="tls-27__list" role="list">
            <li><span class="tls-27__kind" data-kind="new">New</span>Scheduled triggers (cron syntax supported)</li>
            <li><span class="tls-27__kind" data-kind="improved">Improved</span>Run history keeps 90 days on all plans</li>
            <li><span class="tls-27__kind" data-kind="fixed">Fixed</span>Webhook retries respect Retry-After headers</li>
          </ul>
        </article>
        <article class="tls-27__rel">
          <header class="tls-27__rhead"><code class="tls-27__ver">v2.13.2</code><time datetime="2026-06-05">Jun 5</time></header>
          <ul class="tls-27__list" role="list">
            <li><span class="tls-27__kind" data-kind="improved">Improved</span>Keyboard nav: J/K move between rows everywhere</li>
          </ul>
        </article>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
.tls-27,
.tls-27 *,
.tls-27 *::before,
.tls-27 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tls-27 {
  min-height: 100vh;
  width: 100%;
  --tl-accent: #4f46e5;
  --tl-line: #e5e7eb;
  --tl-bg: #ffffff;
  --tl-ink: #111827;
  --tl-mut: #6b7280;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: var(--tl-bg);
  padding: 64px 24px;
  display: grid;
  place-items: center;
}

.tls-27__inner {
  width: min(620px,100%);
}

.tls-27__title {
  font-size: clamp(22px,3.5vw,28px);
  font-weight: 800;
  color: var(--tl-ink);
  letter-spacing: -.02em;
  margin-bottom: 32px;
}

.tls-27__month {
  display: grid;
  grid-template-columns: 64px 1fr;
  gap: 20px;
}

.tls-27__mlabel {
  position: sticky;
  top: 24px;
  align-self: start;
  font-size: 13px;
  font-weight: 800;
  line-height: 1.3;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--tl-mut);
  text-align: right;
  padding-top: 2px;
}

.tls-27__entries {
  position: relative;
  padding: 0 0 36px 26px;
  border-left: 2px solid var(--tl-line);
}

.tls-27__rel {
  position: relative;
  padding-bottom: 28px;
}

.tls-27__rel:last-child {
  padding-bottom: 0;
}

.tls-27__rel::before {
  content: '';
  position: absolute;
  left: -33px;
  top: 5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--tl-bg);
  border: 3px solid var(--tl-accent);
  box-shadow: 0 0 0 4px var(--tl-bg);
}

.tls-27__rel--major::before {
  left: -35px;
  width: 14px;
  height: 14px;
  background: var(--tl-accent);
}

.tls-27__rhead {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 8px;
}

.tls-27__ver {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 13px;
  font-weight: 700;
  color: var(--tl-accent);
  background: #eef2ff;
  border-radius: 6px;
  padding: 2px 8px;
}

.tls-27__rel--major .tls-27__ver {
  color: #fff;
  background: var(--tl-accent);
}

.tls-27__rhead time {
  font-size: 12.5px;
  color: var(--tl-mut);
}

.tls-27__rtitle {
  font-size: 16.5px;
  font-weight: 750;
  color: var(--tl-ink);
  letter-spacing: -.01em;
  margin-bottom: 10px;
}

.tls-27__list {
  list-style: none;
  display: grid;
  gap: 8px;
}

.tls-27__list li {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 14px;
  line-height: 1.55;
  color: #374151;
  text-wrap: pretty;
}

.tls-27__kind {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .04em;
  border-radius: 999px;
  padding: 2px 9px;
  min-width: 64px;
  justify-content: center;
}

.tls-27__kind::before {
  font-weight: 800;
}

.tls-27__kind[data-kind="new"] {
  color: #166534;
  background: #f0fdf4;
}

.tls-27__kind[data-kind="new"]::before {
  content: '+';
}

.tls-27__kind[data-kind="improved"] {
  color: #1d4ed8;
  background: #eff6ff;
}

.tls-27__kind[data-kind="improved"]::before {
  content: '↑';
}

.tls-27__kind[data-kind="fixed"] {
  color: #9a3412;
  background: #fff7ed;
}

.tls-27__kind[data-kind="fixed"]::before {
  content: '✓';
}

@media (max-width: 520px) {
  .tls-27__month {
    grid-template-columns: 1fr;
  }

  .tls-27__mlabel {
    position: static;
    text-align: left;
    padding-bottom: 10px;
  }

  .tls-27__mlabel br {
    display: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — month labels are position:sticky inside their month group; category chips are data-kind attributes rendering glyph + text (never color alone). */
```

How this works

The page is a two-column grid: a narrow gutter column for month labels and a main column of release entries. Month labels are position: sticky; top: 24px INSIDE their month group, so as a month's releases scroll, its label rides along in the gutter, then hands off when the next month begins — the same handoff physics as demo 20, rotated into a side rail.

Each release is an <article> headed by a semver badge (v2.14.0) in monospace and a <time> date. Change categories are data-kind chips where the glyph is drawn per kind (+ for New, ↑ for Improved, ✕→✓ wrench-free 'Fixed' dot) so category survives grayscale — and each chip's text IS its category, never color alone.

The rail dot for each release is ring-punched over the rail line with a box-shadow of the page background, and major releases (x.0.0) get a larger, filled marker via a --major class — visual versioning hierarchy at a glance. Everything themes from four tokens.

Techniques used: sticky gutter labels with per-group containment handoff · semver badges in monospace with major-release marker hierarchy · data-kind category chips (glyph + text, never color-only) · ring-punched rail markers · two-column grid with narrow sticky gutter · time[datetime] machine dates

Make it yours

  • Sticky offset: top:24px on the month label — align it with your site header.
  • Major-release emphasis: the --major marker size/fill; add a subtle card border tint for x.0.0 entries if launches deserve more.
  • Categories: each is one data-kind ruleset — add 'Security' or 'Deprecated' without touching layout.
  • RSS-friendly: each article already has a heading + time; add id="v2-14-0" anchors for deep-linkable releases.

Gotchas — read before shipping

  • The sticky label needs its PARENT (the month group) to be tall enough to scroll within — sticking happens inside the parent's box, never beyond it.
  • Chips must keep their text labels; icon-only category chips fail WCAG 1.1.1 unless you add hidden text.
  • Monospace semver badges: proportional fonts make v1.1.1 and v9.9.9 different widths and the rail alignment wobbles.
  • Don't reverse-order releases oldest-first — changelogs are newest-first by convention and users bounce if v1.0 greets them.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by text-wrap as this demo is written. The core technique itself goes back further — Chrome all.

Sticky positioning, grid and attribute selectors — everything is baseline-everywhere CSS.

Techniques used in this demo

Search CodeFronts

Loading…