You Might Also Like
Scroll Timeline Nav Progress Indicator — Native CSS, No JS
Scroll Timeline Nav Progress Indicator · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Scroll Timeline Nav Progress — A CSS-Only Underline That Tracks Page Scroll

Sticky navigation bars often pair with some form of scroll indicator — a progress bar, an active-link highlight, or both. This snippet builds the progress half with a single native CSS feature: a thin underline beneath the nav that fills left-to-right in lockstep with total page scroll, using animation-timeline: scroll() instead of a scroll event handler doing percentage math on every frame.
A fill bound to the nav, not the viewport
Unlike a full-width top-of-page bar (see CSS Scroll-Driven Progress Bar), this indicator lives inside the nav itself as a 3px .stn-track/.stn-fill pair pinned to the bottom edge. The fill still uses the exact same mechanism — a keyframe animation scaling from scaleX(0) to scaleX(1), retimed by animation-timeline: scroll(root) so its progress equals the document's scroll fraction. Placing it inside a position: sticky nav means it always reads as attached to the navigation, not as a separate UI element competing for attention.
Why it belongs next to a nav
Putting the progress signal directly under the links people are already looking at (rather than a bar at the very top edge of the screen, easy to miss) makes it a more natural companion to navigation — it reads as "how far through this page am I" right where the reader's eyes already rest. It pairs well with a lightweight active-section highlight, which this snippet layers on top using a small, separate scroll listener — deliberately kept apart from the CSS-only fill so the core progress mechanism stays untouched by JavaScript.
Two techniques, cleanly separated
The demo intentionally shows both approaches side by side: the underline fill is 100% CSS and needs no JavaScript to function, while the "which link is active" highlight is a conventional scroll listener, because determining which section a heading belongs to is exactly the kind of layout-dependent logic that native scroll timelines don't yet solve on their own. This is a good habit generally — reach for scroll-driven CSS animations for continuous visual mappings like fills, rotations, or scale, and keep discrete logic (which item is "active") in JavaScript.
Pairing with other scroll effects
Combine this nav with a hero built on Parallax Hero Section below the fold, or a content grid using Scroll Reveal Grid — the nav's underline will continue to reflect total page position regardless of what scroll-triggered animations are happening further down, since it reads directly from the document scroller rather than any particular section.
Customizing it
Swap the underline for a dot that moves along a horizontal track using translateX instead of scaleX, change scroll(root) to a named timeline on an inner scroll container for app-shell layouts, or theme the gradient to match your brand.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to work out where to scope a scroll timeline by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why the underline fill is deliberately kept separate from the active-link highlighting logic — one using animation-timeline: scroll() and the other using a scroll event listener — and what problem each technique is actually good at solving. The same assistant can help you extend it, for instance turning the underline into a moving dot with translateX instead of a scaling fill, or scoping the timeline to an inner scrollable container for an app-shell layout using scroll(nearest) instead of scroll(root). It's also useful for refining the active-link detection, such as replacing the getBoundingClientRect-based check with an IntersectionObserver for more robust section detection on pages with uneven section heights. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a sticky navigation bar with a thin progress underline driven by native CSS scroll-driven animations (animation-timeline: scroll()), plus a separate JavaScript-based active-link highlight — keeping the two techniques clearly separated.
Requirements:
- A position: sticky nav bar containing a brand mark, a list of nav links, and a thin (2-4px) track element pinned to the nav's bottom edge.
- The track's fill element must use a CSS keyframe animation scaling from scaleX(0) to scaleX(1) with transform-origin at the left edge, driven by animation-timeline: scroll(root) (or an equivalent scroll-timeline bound to the document) and animation: <name> auto linear — no scroll event listener may compute this fill.
- Wrap a fallback in @supports not (animation-timeline: scroll()) so unsupported browsers show a static, clearly non-functional state instead of a broken animation.
- Separately, add a small scroll event listener in JavaScript that determines which page section is nearest the top of the viewport and highlights the corresponding nav link's color — this logic must be visibly separate from the CSS fill and must not itself drive the underline's width or scale.
- Include several long content sections below the nav so both the fill and the active-link highlight are clearly demonstrated across a full scroll.
- Make sure resizing the window doesn't require re-running any JavaScript for the underline fill to remain accurate — only the native CSS timeline should be responsible for that.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste the HTML, CSS, and JSA sticky nav with an underline track renders — no CDN needed.
- 2Scroll the pageThe amber-to-orange underline fills beneath the nav links.
- 3Watch the active linkA separate scroll listener highlights the nearest section's link.
- 4Resize the browserThe fill keeps tracking correctly with no JS recalculation needed.
- 5Inspect the CSSOnly .stn-fill uses animation-timeline; the highlight logic is plain JS.
- 6Restyle itSwap the underline for a dot, or change scroll(root) to a container.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Both use the same animation-timeline: scroll(root) mechanism, but this indicator is scoped to sit directly beneath a sticky nav rather than spanning the very top edge of the viewport. Visually it reads as part of the navigation rather than a separate UI element, which suits documentation and product-tour layouts where the nav is already the anchor for orientation.
Determining which section is "current" depends on comparing each section's bounding position to the viewport, which is layout-dependent logic that scroll-driven CSS animations don't express well on their own — CSS timelines are built for continuous value mappings like a fill or rotation, not discrete state like "which of these five items is active." Keeping that logic in a small scroll listener keeps each technique doing what it's best at.
Yes. Because the browser recomputes the scroll(root) timeline's range natively whenever the document's scrollable height changes, resizing the window (which reflows content and changes total page height) doesn't require any JavaScript recalculation — the native timeline stays correct automatically.
Yes — change scroll(root) to scroll(nearest), and make sure the .stn-fill element's nearest scrollable ancestor is the panel you want to track rather than the document. This is useful for app-shell layouts where the nav sits outside a scrollable content pane.
The underline fill is pure CSS and ports directly into any component's stylesheet. The active-link highlight scroll listener moves into a mount effect (useEffect, onMounted, or ngAfterViewInit) and should clean up its scroll listener on unmount to avoid leaks when the nav is unmounted during route changes.