20 / 32
All 32 in 32 CSS Timelines
- 01GitHub-Style Commit History Timeline
- 02CI/CD Deploy Pipeline Timeline — Status Steps with Live Spinner
- 03Resume / Work Experience Timeline with Subgrid Alignment
- 04Order Tracking Timeline — Delivery Progress Stepper
- 05Fitness Activity Feed Timeline — Workout Cards with Metric Grids
- 06Scroll-Driven Reveal Timeline — animation-timeline: view() Entrances
- 07Team Swimlane Gantt Timeline — Grid-Line Placement, No Libraries
- 08Conference Agenda Timeline — Hour Grid with Parallel Tracks
- 09Stratigraphy Timeline — Geological Layer Blocks with Depth Scale
- 10Concert Tour Timeline — Ticket Stub Cards with Perforated Edges
- 11Patient Vitals Timeline — Medical Dashboard with CSS Sparklines
- 12Film Reel Timeline — Sprocket-Hole Border History Strip
- 13Mission Control Timeline — T-Minus Event Log with Status Dots
- 14Fermentation Calendar Timeline — Process Stages as Duration Bars
- 15Conversation Thread Timeline — Nested Replies with Curved Connectors
- 16Accordion Timeline — Expandable Milestones with Exclusive Open
- 17Cinematic Filmstrip Timeline — Scroll-Snap Career Frames
- 18Nature Trail Timeline — Winding Dashed Path with Waypoint Badges
- 19Terminal Log Timeline — Command-Line Session with Typing Caret
- 20Reading-Progress Timeline — animation-timeline: scroll() Article Tracker
- 21Zigzag Alternating Timeline — Container Query Responsive
- 22Tabbed Era Timeline — Radio-Button Period Switcher, Zero JS
- 23Anchor-Nav Timeline — :target Deep-Linkable Milestones
- 24Scrollytelling Timeline — Sticky Chapter Panel with Scrolling Beats
- 25Product Roadmap Timeline — Quarter Columns with Status Chips
- 26Horizontal Panorama Timeline — Vertical Scroll Drives Sideways Travel
- 27SaaS Changelog Timeline — Version Badges with Sticky Month Rail
- 28Editorial History Timeline — Sticky Era Headers in Serif
- 29Comparison Dual Timeline — Two Histories on One Spine
- 30Metro Map Timeline — Transit Lines with Interchange Stations
- 31Flight Itinerary Timeline — Segments, Layover and Timezone Footnotes
- 32Live Activity Feed Timeline — Real-Time Events with :has() Filter
Reading-Progress Timeline — animation-timeline: scroll() Article Tracker
A long-read companion: a progress bar that fills as the article scrolls and a chapter rail whose dots light up as each section passes — both driven by the CSS scroll() and view() timelines, the pure-CSS replacement for the scroll-listener progress bars every publisher ships in JavaScript today.
Build interactive UI like this from scratch: 20 HTML, CSS & JavaScript projects
ZTM · JavaScript Web Projects · 30‑day money-back guarantee · Affiliate link
The code
<section class="tls-20" aria-label="Reading progress timeline demo">
<div class="tls-20__frame">
<div class="tls-20__scroller" tabindex="0">
<header class="tls-20__bar">
<p class="tls-20__label">The State of CSS · 12 min read</p>
<div class="tls-20__track" aria-hidden="true"><div class="tls-20__fill"></div></div>
</header>
<article class="tls-20__article">
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>1 · The layout wars are over</h4>
<p>Grid and flexbox settled the argument a decade ago, and the last float-based holdouts have quietly refactored. What remains is choosing the right tool: one-dimensional flow to flexbox, two-dimensional structure to grid, and — since subgrid — nested structure to the parent's tracks.</p>
<p>The practical consequence is that layout bugs have become placement bugs. We no longer fight the engine; we mis-declare our intent to it. That is enormous progress.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>2 · Components query themselves</h4>
<p>Container queries ended the era of the viewport as the only breakpoint. A card now reshapes for the sidebar it lives in, not the monitor it happens to be displayed on — and design systems finally ship components that are honestly portable.</p>
<p>The mental shift matters more than the syntax: responsive design becomes a property of the component, not of the page.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>3 · The scrollbar becomes a timeline</h4>
<p>Scroll-driven animations moved an entire genre of JavaScript — progress bars, reveal-on-scroll, parallax — into declarative CSS that runs off the main thread. This very progress bar is one of them: no listener, no rAF loop, no jank budget.</p>
<p>The rule of thumb: if an effect is a pure function of scroll position, it belongs to the compositor now.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>4 · What to learn next</h4>
<p>:has() for relational styling, view transitions for state changes, anchor positioning for tooltips and menus. None of them are exotic anymore — they are simply what CSS is.</p>
</section>
</article>
</div>
</div>
</section><section class="tls-20" aria-label="Reading progress timeline demo">
<div class="tls-20__frame">
<div class="tls-20__scroller" tabindex="0">
<header class="tls-20__bar">
<p class="tls-20__label">The State of CSS · 12 min read</p>
<div class="tls-20__track" aria-hidden="true"><div class="tls-20__fill"></div></div>
</header>
<article class="tls-20__article">
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>1 · The layout wars are over</h4>
<p>Grid and flexbox settled the argument a decade ago, and the last float-based holdouts have quietly refactored. What remains is choosing the right tool: one-dimensional flow to flexbox, two-dimensional structure to grid, and — since subgrid — nested structure to the parent's tracks.</p>
<p>The practical consequence is that layout bugs have become placement bugs. We no longer fight the engine; we mis-declare our intent to it. That is enormous progress.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>2 · Components query themselves</h4>
<p>Container queries ended the era of the viewport as the only breakpoint. A card now reshapes for the sidebar it lives in, not the monitor it happens to be displayed on — and design systems finally ship components that are honestly portable.</p>
<p>The mental shift matters more than the syntax: responsive design becomes a property of the component, not of the page.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>3 · The scrollbar becomes a timeline</h4>
<p>Scroll-driven animations moved an entire genre of JavaScript — progress bars, reveal-on-scroll, parallax — into declarative CSS that runs off the main thread. This very progress bar is one of them: no listener, no rAF loop, no jank budget.</p>
<p>The rule of thumb: if an effect is a pure function of scroll position, it belongs to the compositor now.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>4 · What to learn next</h4>
<p>:has() for relational styling, view transitions for state changes, anchor positioning for tooltips and menus. None of them are exotic anymore — they are simply what CSS is.</p>
</section>
</article>
</div>
</div>
</section>.tls-20,
.tls-20 *,
.tls-20 *::before,
.tls-20 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tls-20 {
min-height: 100vh;
width: 100%;
--tl-accent: #c2410c;
--tl-line: #f3e5dc;
--tl-ink: #2b1d14;
--tl-mut: #8a7263;
font-family: Georgia,'Times New Roman',serif;
background: #fdf9f6;
padding: 64px 24px;
display: grid;
place-items: center;
}
.tls-20__frame {
width: min(620px,100%);
}
.tls-20__scroller {
height: 420px;
overflow-y: auto;
background: #fff;
border: 1px solid var(--tl-line);
border-radius: 16px;
box-shadow: 0 10px 34px -22px rgb(43 29 20 / .3);
}
.tls-20__bar {
position: sticky;
top: 0;
z-index: 2;
background: #fff;
padding: 14px 26px 0;
border-bottom: 1px solid var(--tl-line);
}
.tls-20__label {
font-family: 'Segoe UI',system-ui,sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--tl-mut);
padding-bottom: 10px;
}
.tls-20__track {
height: 4px;
background: var(--tl-line);
border-radius: 2px 2px 0 0;
overflow: hidden;
}
.tls-20__fill {
height: 100%;
background: var(--tl-accent);
transform-origin: left;
}
.tls-20__article {
padding: 10px 26px 30px;
}
.tls-20__sec {
padding: 18px 0 10px;
}
.tls-20__h {
display: flex;
align-items: center;
gap: 10px;
font-family: 'Segoe UI',system-ui,sans-serif;
font-size: 14px;
font-weight: 800;
color: var(--tl-ink);
letter-spacing: -.01em;
margin-bottom: 10px;
}
.tls-20__dot {
flex: none;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2.5px solid var(--tl-accent);
background: transparent;
}
.tls-20__sec p {
font-size: 15.5px;
line-height: 1.7;
color: #4a3a2e;
margin-bottom: 12px;
text-wrap: pretty;
}
@supports (animation-timeline: scroll()) {
.tls-20__fill {
transform: scaleX(0);
animation: tls-20-grow linear both;
animation-timeline: scroll(nearest);
}
@keyframes tls-20-grow {
to {
transform: scaleX(1);
}
}
.tls-20__sec {
view-timeline-name: --tls25-sec;
}
.tls-20__dot {
animation: tls-20-dot linear both;
animation-timeline: view(nearest);
animation-range: entry 0% entry 100%;
}
@keyframes tls-20-dot {
to {
background: var(--tl-accent);
}
}
}.tls-20,
.tls-20 *,
.tls-20 *::before,
.tls-20 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tls-20 {
min-height: 100vh;
width: 100%;
--tl-accent: #c2410c;
--tl-line: #f3e5dc;
--tl-ink: #2b1d14;
--tl-mut: #8a7263;
font-family: Georgia,'Times New Roman',serif;
background: #fdf9f6;
padding: 64px 24px;
display: grid;
place-items: center;
}
.tls-20__frame {
width: min(620px,100%);
}
.tls-20__scroller {
height: 420px;
overflow-y: auto;
background: #fff;
border: 1px solid var(--tl-line);
border-radius: 16px;
box-shadow: 0 10px 34px -22px rgb(43 29 20 / .3);
}
.tls-20__bar {
position: sticky;
top: 0;
z-index: 2;
background: #fff;
padding: 14px 26px 0;
border-bottom: 1px solid var(--tl-line);
}
.tls-20__label {
font-family: 'Segoe UI',system-ui,sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--tl-mut);
padding-bottom: 10px;
}
.tls-20__track {
height: 4px;
background: var(--tl-line);
border-radius: 2px 2px 0 0;
overflow: hidden;
}
.tls-20__fill {
height: 100%;
background: var(--tl-accent);
transform-origin: left;
}
.tls-20__article {
padding: 10px 26px 30px;
}
.tls-20__sec {
padding: 18px 0 10px;
}
.tls-20__h {
display: flex;
align-items: center;
gap: 10px;
font-family: 'Segoe UI',system-ui,sans-serif;
font-size: 14px;
font-weight: 800;
color: var(--tl-ink);
letter-spacing: -.01em;
margin-bottom: 10px;
}
.tls-20__dot {
flex: none;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2.5px solid var(--tl-accent);
background: transparent;
}
.tls-20__sec p {
font-size: 15.5px;
line-height: 1.7;
color: #4a3a2e;
margin-bottom: 12px;
text-wrap: pretty;
}
@supports (animation-timeline: scroll()) {
.tls-20__fill {
transform: scaleX(0);
animation: tls-20-grow linear both;
animation-timeline: scroll(nearest);
}
@keyframes tls-20-grow {
to {
transform: scaleX(1);
}
}
.tls-20__sec {
view-timeline-name: --tls25-sec;
}
.tls-20__dot {
animation: tls-20-dot linear both;
animation-timeline: view(nearest);
animation-range: entry 0% entry 100%;
}
@keyframes tls-20-dot {
to {
background: var(--tl-accent);
}
}
}/* No JavaScript — the bar is animation-timeline: scroll(nearest) with a scaleX fill (compositor-only); chapter dots fill against per-section view() timelines. Remove the demo's fixed-height scroller and the same CSS tracks the page. */
/* No JavaScript — the bar is animation-timeline: scroll(nearest) with a scaleX fill (compositor-only); chapter dots fill against per-section view() timelines. Remove the demo's fixed-height scroller and the same CSS tracks the page. */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: Reading-Progress Timeline — animation-timeline: scroll() Article Tracker
Source: https://codefronts.com/layouts/css-timelines/art-deco-steps/
A long-read companion: a progress bar that fills as the article scrolls and a chapter rail whose dots light up as each section passes — both driven by the CSS scroll() and view() timelines, the pure-CSS replacement for the scroll-listener progress bars every publisher ships in JavaScript today.
## HTML
```html
<section class="tls-20" aria-label="Reading progress timeline demo">
<div class="tls-20__frame">
<div class="tls-20__scroller" tabindex="0">
<header class="tls-20__bar">
<p class="tls-20__label">The State of CSS · 12 min read</p>
<div class="tls-20__track" aria-hidden="true"><div class="tls-20__fill"></div></div>
</header>
<article class="tls-20__article">
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>1 · The layout wars are over</h4>
<p>Grid and flexbox settled the argument a decade ago, and the last float-based holdouts have quietly refactored. What remains is choosing the right tool: one-dimensional flow to flexbox, two-dimensional structure to grid, and — since subgrid — nested structure to the parent's tracks.</p>
<p>The practical consequence is that layout bugs have become placement bugs. We no longer fight the engine; we mis-declare our intent to it. That is enormous progress.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>2 · Components query themselves</h4>
<p>Container queries ended the era of the viewport as the only breakpoint. A card now reshapes for the sidebar it lives in, not the monitor it happens to be displayed on — and design systems finally ship components that are honestly portable.</p>
<p>The mental shift matters more than the syntax: responsive design becomes a property of the component, not of the page.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>3 · The scrollbar becomes a timeline</h4>
<p>Scroll-driven animations moved an entire genre of JavaScript — progress bars, reveal-on-scroll, parallax — into declarative CSS that runs off the main thread. This very progress bar is one of them: no listener, no rAF loop, no jank budget.</p>
<p>The rule of thumb: if an effect is a pure function of scroll position, it belongs to the compositor now.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>4 · What to learn next</h4>
<p>:has() for relational styling, view transitions for state changes, anchor positioning for tooltips and menus. None of them are exotic anymore — they are simply what CSS is.</p>
</section>
</article>
</div>
</div>
</section>
```
## CSS
```css
.tls-20,
.tls-20 *,
.tls-20 *::before,
.tls-20 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tls-20 {
min-height: 100vh;
width: 100%;
--tl-accent: #c2410c;
--tl-line: #f3e5dc;
--tl-ink: #2b1d14;
--tl-mut: #8a7263;
font-family: Georgia,'Times New Roman',serif;
background: #fdf9f6;
padding: 64px 24px;
display: grid;
place-items: center;
}
.tls-20__frame {
width: min(620px,100%);
}
.tls-20__scroller {
height: 420px;
overflow-y: auto;
background: #fff;
border: 1px solid var(--tl-line);
border-radius: 16px;
box-shadow: 0 10px 34px -22px rgb(43 29 20 / .3);
}
.tls-20__bar {
position: sticky;
top: 0;
z-index: 2;
background: #fff;
padding: 14px 26px 0;
border-bottom: 1px solid var(--tl-line);
}
.tls-20__label {
font-family: 'Segoe UI',system-ui,sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--tl-mut);
padding-bottom: 10px;
}
.tls-20__track {
height: 4px;
background: var(--tl-line);
border-radius: 2px 2px 0 0;
overflow: hidden;
}
.tls-20__fill {
height: 100%;
background: var(--tl-accent);
transform-origin: left;
}
.tls-20__article {
padding: 10px 26px 30px;
}
.tls-20__sec {
padding: 18px 0 10px;
}
.tls-20__h {
display: flex;
align-items: center;
gap: 10px;
font-family: 'Segoe UI',system-ui,sans-serif;
font-size: 14px;
font-weight: 800;
color: var(--tl-ink);
letter-spacing: -.01em;
margin-bottom: 10px;
}
.tls-20__dot {
flex: none;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2.5px solid var(--tl-accent);
background: transparent;
}
.tls-20__sec p {
font-size: 15.5px;
line-height: 1.7;
color: #4a3a2e;
margin-bottom: 12px;
text-wrap: pretty;
}
@supports (animation-timeline: scroll()) {
.tls-20__fill {
transform: scaleX(0);
animation: tls-20-grow linear both;
animation-timeline: scroll(nearest);
}
@keyframes tls-20-grow {
to {
transform: scaleX(1);
}
}
.tls-20__sec {
view-timeline-name: --tls25-sec;
}
.tls-20__dot {
animation: tls-20-dot linear both;
animation-timeline: view(nearest);
animation-range: entry 0% entry 100%;
}
@keyframes tls-20-dot {
to {
background: var(--tl-accent);
}
}
}
```
## JavaScript
```js
/* No JavaScript — the bar is animation-timeline: scroll(nearest) with a scaleX fill (compositor-only); chapter dots fill against per-section view() timelines. Remove the demo's fixed-height scroller and the same CSS tracks the page. */
```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: Reading-Progress Timeline — animation-timeline: scroll() Article Tracker
Source: https://codefronts.com/layouts/css-timelines/art-deco-steps/
A long-read companion: a progress bar that fills as the article scrolls and a chapter rail whose dots light up as each section passes — both driven by the CSS scroll() and view() timelines, the pure-CSS replacement for the scroll-listener progress bars every publisher ships in JavaScript today.
## HTML
```html
<section class="tls-20" aria-label="Reading progress timeline demo">
<div class="tls-20__frame">
<div class="tls-20__scroller" tabindex="0">
<header class="tls-20__bar">
<p class="tls-20__label">The State of CSS · 12 min read</p>
<div class="tls-20__track" aria-hidden="true"><div class="tls-20__fill"></div></div>
</header>
<article class="tls-20__article">
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>1 · The layout wars are over</h4>
<p>Grid and flexbox settled the argument a decade ago, and the last float-based holdouts have quietly refactored. What remains is choosing the right tool: one-dimensional flow to flexbox, two-dimensional structure to grid, and — since subgrid — nested structure to the parent's tracks.</p>
<p>The practical consequence is that layout bugs have become placement bugs. We no longer fight the engine; we mis-declare our intent to it. That is enormous progress.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>2 · Components query themselves</h4>
<p>Container queries ended the era of the viewport as the only breakpoint. A card now reshapes for the sidebar it lives in, not the monitor it happens to be displayed on — and design systems finally ship components that are honestly portable.</p>
<p>The mental shift matters more than the syntax: responsive design becomes a property of the component, not of the page.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>3 · The scrollbar becomes a timeline</h4>
<p>Scroll-driven animations moved an entire genre of JavaScript — progress bars, reveal-on-scroll, parallax — into declarative CSS that runs off the main thread. This very progress bar is one of them: no listener, no rAF loop, no jank budget.</p>
<p>The rule of thumb: if an effect is a pure function of scroll position, it belongs to the compositor now.</p>
</section>
<section class="tls-20__sec">
<h4 class="tls-20__h"><span class="tls-20__dot" aria-hidden="true"></span>4 · What to learn next</h4>
<p>:has() for relational styling, view transitions for state changes, anchor positioning for tooltips and menus. None of them are exotic anymore — they are simply what CSS is.</p>
</section>
</article>
</div>
</div>
</section>
```
## CSS
```css
.tls-20,
.tls-20 *,
.tls-20 *::before,
.tls-20 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tls-20 {
min-height: 100vh;
width: 100%;
--tl-accent: #c2410c;
--tl-line: #f3e5dc;
--tl-ink: #2b1d14;
--tl-mut: #8a7263;
font-family: Georgia,'Times New Roman',serif;
background: #fdf9f6;
padding: 64px 24px;
display: grid;
place-items: center;
}
.tls-20__frame {
width: min(620px,100%);
}
.tls-20__scroller {
height: 420px;
overflow-y: auto;
background: #fff;
border: 1px solid var(--tl-line);
border-radius: 16px;
box-shadow: 0 10px 34px -22px rgb(43 29 20 / .3);
}
.tls-20__bar {
position: sticky;
top: 0;
z-index: 2;
background: #fff;
padding: 14px 26px 0;
border-bottom: 1px solid var(--tl-line);
}
.tls-20__label {
font-family: 'Segoe UI',system-ui,sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--tl-mut);
padding-bottom: 10px;
}
.tls-20__track {
height: 4px;
background: var(--tl-line);
border-radius: 2px 2px 0 0;
overflow: hidden;
}
.tls-20__fill {
height: 100%;
background: var(--tl-accent);
transform-origin: left;
}
.tls-20__article {
padding: 10px 26px 30px;
}
.tls-20__sec {
padding: 18px 0 10px;
}
.tls-20__h {
display: flex;
align-items: center;
gap: 10px;
font-family: 'Segoe UI',system-ui,sans-serif;
font-size: 14px;
font-weight: 800;
color: var(--tl-ink);
letter-spacing: -.01em;
margin-bottom: 10px;
}
.tls-20__dot {
flex: none;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2.5px solid var(--tl-accent);
background: transparent;
}
.tls-20__sec p {
font-size: 15.5px;
line-height: 1.7;
color: #4a3a2e;
margin-bottom: 12px;
text-wrap: pretty;
}
@supports (animation-timeline: scroll()) {
.tls-20__fill {
transform: scaleX(0);
animation: tls-20-grow linear both;
animation-timeline: scroll(nearest);
}
@keyframes tls-20-grow {
to {
transform: scaleX(1);
}
}
.tls-20__sec {
view-timeline-name: --tls25-sec;
}
.tls-20__dot {
animation: tls-20-dot linear both;
animation-timeline: view(nearest);
animation-range: entry 0% entry 100%;
}
@keyframes tls-20-dot {
to {
background: var(--tl-accent);
}
}
}
```
## JavaScript
```js
/* No JavaScript — the bar is animation-timeline: scroll(nearest) with a scaleX fill (compositor-only); chapter dots fill against per-section view() timelines. Remove the demo's fixed-height scroller and the same CSS tracks the page. */
```How this works
The article lives in its own scroller (overflow-y:auto with a fixed height — in production this is simply the page). The progress bar declares animation: scaleX 0→1; animation-timeline: scroll(nearest) — its fill is mapped to the scroller's scroll range, updating on the compositor with zero main-thread work. Compare that to the traditional scroll-listener version, which runs JS on every frame and janks precisely when the page is busiest.
The chapter dots use the companion API: each section declares view-timeline-name: --tls25-sec, and its dot (a sibling-referenced element inside the section header) animates its fill against animation-timeline: view() with animation-range: entry 0% entry 100% — the dot pours full as its chapter enters the reading zone, and drains if you scroll back. Progress becomes spatial, bidirectional and truthful, because it IS the scroll position rather than a debounced estimate of it.
The whole enhancement sits inside @supports (animation-timeline: scroll()): unsupported browsers get the identical article with a static rail — nothing breaks, nothing hides. transform: scaleX (not width) keeps the bar's animation compositor-only.
Techniques used: animation-timeline: scroll(nearest) progress fill · scaleX transform fill (compositor-only, never width) · per-section view() timelines filling chapter dots · animation-range entry mapping · @supports progressive enhancement · sticky in-scroller header housing the bar
Make it yours
- Track the PAGE instead of a demo box: remove the fixed height/overflow from the scroller and the same CSS tracks the root scroller.
- Fill origin: transform-origin left — center gives a bar growing outward from the middle.
- Chapter granularity: any element can own a view-timeline; track h2s in prose without restructuring.
- Bar thickness/color: two declarations on the fill; it inherits the accent token.
Gotchas — read before shipping
- scroll(nearest) finds the nearest scrollable ANCESTOR — if a wrapper gains overflow for any reason, the bar starts tracking the wrong scroller. scroll(root) pins it to the page explicitly.
- Animate scaleX, not width — width invalidates layout every scroll frame and defeats the entire point of compositor-driven timelines.
- The @supports gate must wrap the initial scaleX(0) too, or unsupported browsers render a permanently empty bar.
- A progress bar inside an inner scroller needs the bar INSIDE the scroller (sticky), not outside it — outside, scroll(nearest) resolves to the wrong element.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 117+ | 26+ | not yet | 117+ |
Floor set by :has(), animation-timeline, subgrid, text-wrap as this demo is written. The core technique itself goes back further — Chrome 115+.
Scroll-driven animations are Baseline newly-available 2025; the @supports gate leaves other browsers with the identical static article.