32 CSS Timelines14 / 32
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
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 & taste</li>
</ol>
<div class="tls-14__nowline" aria-hidden="true"><span>today</span></div>
</div>
</section><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 & 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;
}
}.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. */
/* 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. */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 & 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. */
```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 & 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-rowor 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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).