32 CSS Timelines14 / 32

Pure CSSMIT licensed

Fermentation Calendar Timeline — Process Stages as Duration Bars

A Gantt in an apron: process stages (primary ferment, cold crash, conditioning) drawn as duration bars on a shared week grid, each bar positioned and sized purely by grid-column line math from inline tokens — the duration-first timeline for recipes, aging processes, brewing logs and any 'what happens when' production cycle.

Published Updated

Live Demo
Try it

The code

<section class="tls-14" aria-label="Fermentation calendar timeline demo">
  <div class="tls-14__board" style="--tl-weeks:10;--tl-nowweek:4">
    <header class="tls-14__head">
      <h3 class="tls-14__title">Saison № 12 — cellar calendar</h3>
      <p class="tls-14__sub">Pitched <time datetime="2026-06-21">Jun 21</time> · week 4 of 10</p>
    </header>
    <div class="tls-14__weeks" aria-hidden="true"><span>W1</span><span>W2</span><span>W3</span><span>W4</span><span>W5</span><span>W6</span><span>W7</span><span>W8</span><span>W9</span><span>W10</span></div>
    <ol class="tls-14__chart" role="list">
      <li class="tls-14__bar" style="--s:1;--d:2;--mix:0%" data-temp="21°C ambient">Primary ferment</li>
      <li class="tls-14__bar" style="--s:3;--d:1;--mix:18%" data-temp="21°C · gravity check">Diacetyl rest</li>
      <li class="tls-14__bar" style="--s:4;--d:2;--mix:32%" data-temp="2°C crash">Cold crash</li>
      <li class="tls-14__bar" style="--s:6;--d:3;--mix:48%" data-temp="12°C · 2.4 vol CO₂">Bottle conditioning</li>
      <li class="tls-14__bar" style="--s:9;--d:2;--mix:64%" data-temp="8°C cellar">Rest &amp; taste</li>
    </ol>
    <div class="tls-14__nowline" aria-hidden="true"><span>today</span></div>
  </div>
</section>
.tls-14,
.tls-14 *,
.tls-14 *::before,
.tls-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tls-14 {
  min-height: 100vh;
  width: 100%;
  --tl-accent: #c2410c;
  --tl-paper: #fdf9f2;
  --tl-ink: #3d2f24;
  --tl-mut: #a08b76;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #efe7da;
  padding: 56px 24px;
  display: grid;
  place-items: center;
}

.tls-14__board {
  width: min(680px,100%);
  position: relative;
  background: var(--tl-paper);
  border: 1px solid #e2d5c2;
  border-radius: 14px;
  padding: 24px 28px 30px;
}

.tls-14__head {
  margin-bottom: 20px;
}

.tls-14__title {
  font-family: Georgia,serif;
  font-size: clamp(18px,3vw,23px);
  font-weight: 700;
  color: var(--tl-ink);
}

.tls-14__sub {
  font-size: 12.5px;
  color: var(--tl-mut);
  margin-top: 3px;
}

.tls-14__weeks {
  display: grid;
  grid-template-columns: repeat(var(--tl-weeks),1fr);
  font-family: ui-monospace,Menlo,monospace;
  font-size: 9.5px;
  color: var(--tl-mut);
  text-align: center;
  margin-bottom: 6px;
}

.tls-14__chart {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(var(--tl-weeks),1fr);
  grid-auto-rows: 38px;
  row-gap: 8px;
  background: repeating-linear-gradient(90deg,transparent 0 calc(100% / var(--tl-weeks) - 1px),#eadfce calc(100% / var(--tl-weeks) - 1px) calc(100% / var(--tl-weeks)));
  border-radius: 8px;
  position: relative;
  padding-block: 8px;
}

.tls-14__bar {
  grid-column: var(--s) / span var(--d);
  position: relative;
  display: flex;
  align-items: center;
  background: color-mix(in oklab,var(--tl-accent),#f5e0c8 var(--mix));
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .01em;
  border-radius: 7px;
  padding-inline: 12px;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-shadow: inset 0 -2px 0 rgba(60,30,0,.18);
  cursor: default;
}

.tls-14__bar[style*="--mix:48%"],
.tls-14__bar[style*="--mix:64%"] {
  color: #5c3a1e;
}

.tls-14__bar:hover::after,
.tls-14__bar:focus-visible::after {
  content: attr(data-temp);
  position: absolute;
  left: 10px;
  bottom: calc(100% + 6px);
  z-index: 2;
  background: var(--tl-ink);
  color: var(--tl-paper);
  font-size: 10.5px;
  font-weight: 600;
  border-radius: 6px;
  padding: 4px 9px;
  white-space: nowrap;
}

.tls-14__nowline {
  position: absolute;
  top: 86px;
  bottom: 24px;
  left: calc(28px + (100% - 56px) * (var(--tl-nowweek) - .5) / var(--tl-weeks));
  width: 2px;
  background: #16a34a;
  border-radius: 1px;
}

.tls-14__nowline span {
  position: absolute;
  top: -16px;
  left: 50%;
  translate: -50% 0;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #16a34a;
}

@media (max-width: 520px) {
  .tls-14__weeks span:nth-child(odd) {
    visibility: hidden;
  }
}
/* No JavaScript — each bar is grid-column:var(--s)/span var(--d); the week ruling and the today-line divide by the same --tl-weeks token, so scale changes stay in sync. */
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: Fermentation Calendar Timeline — Process Stages as Duration Bars
Source: https://codefronts.com/layouts/css-timelines/fermentation-calendar/

A Gantt in an apron: process stages (primary ferment, cold crash, conditioning) drawn as duration bars on a shared week grid, each bar positioned and sized purely by grid-column line math from inline tokens — the duration-first timeline for recipes, aging processes, brewing logs and any 'what happens when' production cycle.
## HTML
```html
<section class="tls-14" aria-label="Fermentation calendar timeline demo">
  <div class="tls-14__board" style="--tl-weeks:10;--tl-nowweek:4">
    <header class="tls-14__head">
      <h3 class="tls-14__title">Saison № 12 — cellar calendar</h3>
      <p class="tls-14__sub">Pitched <time datetime="2026-06-21">Jun 21</time> · week 4 of 10</p>
    </header>
    <div class="tls-14__weeks" aria-hidden="true"><span>W1</span><span>W2</span><span>W3</span><span>W4</span><span>W5</span><span>W6</span><span>W7</span><span>W8</span><span>W9</span><span>W10</span></div>
    <ol class="tls-14__chart" role="list">
      <li class="tls-14__bar" style="--s:1;--d:2;--mix:0%" data-temp="21°C ambient">Primary ferment</li>
      <li class="tls-14__bar" style="--s:3;--d:1;--mix:18%" data-temp="21°C · gravity check">Diacetyl rest</li>
      <li class="tls-14__bar" style="--s:4;--d:2;--mix:32%" data-temp="2°C crash">Cold crash</li>
      <li class="tls-14__bar" style="--s:6;--d:3;--mix:48%" data-temp="12°C · 2.4 vol CO₂">Bottle conditioning</li>
      <li class="tls-14__bar" style="--s:9;--d:2;--mix:64%" data-temp="8°C cellar">Rest &amp; taste</li>
    </ol>
    <div class="tls-14__nowline" aria-hidden="true"><span>today</span></div>
  </div>
</section>
```
## CSS
```css
.tls-14,
.tls-14 *,
.tls-14 *::before,
.tls-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tls-14 {
  min-height: 100vh;
  width: 100%;
  --tl-accent: #c2410c;
  --tl-paper: #fdf9f2;
  --tl-ink: #3d2f24;
  --tl-mut: #a08b76;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #efe7da;
  padding: 56px 24px;
  display: grid;
  place-items: center;
}

.tls-14__board {
  width: min(680px,100%);
  position: relative;
  background: var(--tl-paper);
  border: 1px solid #e2d5c2;
  border-radius: 14px;
  padding: 24px 28px 30px;
}

.tls-14__head {
  margin-bottom: 20px;
}

.tls-14__title {
  font-family: Georgia,serif;
  font-size: clamp(18px,3vw,23px);
  font-weight: 700;
  color: var(--tl-ink);
}

.tls-14__sub {
  font-size: 12.5px;
  color: var(--tl-mut);
  margin-top: 3px;
}

.tls-14__weeks {
  display: grid;
  grid-template-columns: repeat(var(--tl-weeks),1fr);
  font-family: ui-monospace,Menlo,monospace;
  font-size: 9.5px;
  color: var(--tl-mut);
  text-align: center;
  margin-bottom: 6px;
}

.tls-14__chart {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(var(--tl-weeks),1fr);
  grid-auto-rows: 38px;
  row-gap: 8px;
  background: repeating-linear-gradient(90deg,transparent 0 calc(100% / var(--tl-weeks) - 1px),#eadfce calc(100% / var(--tl-weeks) - 1px) calc(100% / var(--tl-weeks)));
  border-radius: 8px;
  position: relative;
  padding-block: 8px;
}

.tls-14__bar {
  grid-column: var(--s) / span var(--d);
  position: relative;
  display: flex;
  align-items: center;
  background: color-mix(in oklab,var(--tl-accent),#f5e0c8 var(--mix));
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .01em;
  border-radius: 7px;
  padding-inline: 12px;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-shadow: inset 0 -2px 0 rgba(60,30,0,.18);
  cursor: default;
}

.tls-14__bar[style*="--mix:48%"],
.tls-14__bar[style*="--mix:64%"] {
  color: #5c3a1e;
}

.tls-14__bar:hover::after,
.tls-14__bar:focus-visible::after {
  content: attr(data-temp);
  position: absolute;
  left: 10px;
  bottom: calc(100% + 6px);
  z-index: 2;
  background: var(--tl-ink);
  color: var(--tl-paper);
  font-size: 10.5px;
  font-weight: 600;
  border-radius: 6px;
  padding: 4px 9px;
  white-space: nowrap;
}

.tls-14__nowline {
  position: absolute;
  top: 86px;
  bottom: 24px;
  left: calc(28px + (100% - 56px) * (var(--tl-nowweek) - .5) / var(--tl-weeks));
  width: 2px;
  background: #16a34a;
  border-radius: 1px;
}

.tls-14__nowline span {
  position: absolute;
  top: -16px;
  left: 50%;
  translate: -50% 0;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #16a34a;
}

@media (max-width: 520px) {
  .tls-14__weeks span:nth-child(odd) {
    visibility: hidden;
  }
}
```

## JavaScript
```js
/* No JavaScript — each bar is grid-column:var(--s)/span var(--d); the week ruling and the today-line divide by the same --tl-weeks token, so scale changes stay in sync. */
```

How this works

The whole chart is one CSS grid: grid-template-columns: repeat(var(--tl-weeks), 1fr) with a labeled header row. Each stage bar is an <li> placed with grid-column: var(--s) / span var(--d) — start week and duration as two inline integers. That's the entire engine: no absolute positioning, no percentage math, no JS layout pass. Change --d:3 to --d:4 and the bar grows exactly one week.

The week grid behind the bars is a repeating-linear-gradient sized calc(100% / var(--tl-weeks)) so gridlines always agree with the column math. 'You are here' is a vertical rule positioned with the same token (--tl-nowweek) — one source of truth for the time scale, three consumers.

Each bar carries its stage name inline (with text-overflow:ellipsis for narrow spans) plus temperature notes in a hover/focus tooltip built from a positioned ::after reading attr(data-temp) — data lives in the markup, presentation in the stylesheet. Bars are real list items in process order, so the recipe reads correctly linearized.

Techniques used: grid-column:var(--s)/span var(--d) duration bars (integer-token Gantt) · repeating-linear-gradient week ruling synced by the same --tl-weeks token · attr()-driven tooltips from data-attributes · shared --tl-nowweek current-time rule · ol/li process semantics · color-mix() stage tint ramp

Make it yours

  • Timescale: set --tl-weeks (columns), and every bar keeps its --s/--d meaning — a 12-week cider or 36-month whisky is the same markup.
  • Resolution: columns can be days — relabel the header and treat --s/--d as day indices.
  • Overlapping stages (dry-hop during conditioning): give rows explicit grid-row or let auto-placement stack them — both work.
  • Tint ramp: bars derive from one --tl-accent via color-mix() percentages — one token retints the chart.

Gotchas — read before shipping

  • grid-column lines are 1-indexed and the END line is exclusive — a stage starting week 1 lasting 2 weeks is --s:1;--d:2 → lines 1/3; off-by-ones here are THE classic Gantt bug.
  • The gradient ruling must divide by the same --tl-weeks the grid uses — hardcode 10% with 12 columns and lines drift off the cells.
  • attr() tooltips are hover/focus-only content: keep anything essential (temps, timings) also in visible text or the linearized recipe below.
  • Give bars min-width:0 and ellipsis — long stage names in 1-week bars otherwise blow the column out.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

Floor set by color-mix() as this demo is written. The core technique itself goes back further — Chrome all.

Grid line placement from custom properties is baseline-everywhere; color-mix() tints are 2023 baseline (hex fallbacks trivial).

Techniques used in this demo

Search CodeFronts

Loading…