webdevpuneet.com

Heading Underline Read Progress — Native CSS view-timeline

Heading Underline Fill (view-timeline) · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<article class="hup-article">
  <header class="hup-header"><h1>Read to Fill the Line</h1><p>Scroll down. The underline beneath each heading below fills left-to-right as you scroll past that heading's own paragraph, driven by a native CSS <code>view-timeline</code> attached to the paragraph itself — no scroll listener involved.</p></header>

  <section class="hup-section">
    <h2 class="hup-heading">The Problem With Manual Trust</h2>
    <div class="hup-underline"></div>
    <p class="hup-source">Every workflow that depends on a human remembering to double-check something eventually fails, not because people are careless but because attention is a finite resource that gets spent elsewhere under deadline pressure. Systems that rely on vigilance alone are systems that are quietly waiting to fail at the worst possible time, and the fix is never "try harder" — it is removing the step where a human has to remember at all.</p>
  </section>

  <section class="hup-section">
    <h2 class="hup-heading">Designing for the Absent-Minded Case</h2>
    <div class="hup-underline"></div>
    <p class="hup-source">The strongest interfaces assume the user is distracted, in a hurry, and on a phone with one bar of signal. Defaults should be safe, destructive actions should require confirmation proportional to their cost, and state that matters should be visible without requiring a click to reveal it. Design for the tired eleven-p.m. version of your user, not the attentive one reading the documentation.</p>
  </section>

  <section class="hup-section">
    <h2 class="hup-heading">Why Progress Should Be Legible</h2>
    <div class="hup-underline"></div>
    <p class="hup-source">A reader who cannot tell how much of a page remains will either abandon early out of uncertainty or skim past details assuming there is more time than there is. A visible sense of progress — a bar, a counter, or in this case a filling underline tied to how much of a specific passage has been read — keeps the reader oriented without requiring them to scroll back and check.</p>
  </section>
</article>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

A view-timeline source declared on one element (the paragraph) driving another (the underline)
view-timeline-name resolved by name across sibling elements, not just self-referencing
animation-range narrows completion to a plausible "finished reading" point, not the literal geometric edge
Independent per-paragraph timelines — each underline tracks only its own passage
transform: scaleX() fill is compositor-friendly and stays smooth during fast scrolling
Zero JavaScript scroll or intersection handling
@supports fallback shows every underline fully filled rather than stuck empty
Reusable on any heading/paragraph pair with no per-instance JavaScript wiring

About this UI Snippet

Heading Underline Read Progress — animation-timeline: view() Bound to a Different Element

Screenshot of the Heading Underline Fill (view-timeline) snippet rendered live

Most scroll-driven CSS demos bind an element's animation to its own view timeline — the element watches itself. This snippet shows the less commonly demonstrated but equally native pattern: a view timeline whose *source* is one element (a paragraph) driving an animation on a completely different element (a heading's underline bar), so the underline fills in step with how far the reader has scrolled through that specific block of text, not with the underline's own tiny transit through the viewport.

Splitting the timeline source from the animated element

view-timeline-name: --para-read and view-timeline-axis: block are declared on .hup-source, the <p> element — this makes the paragraph the *source* of a named timeline representing its own scroll-driven visibility, from first entering to fully exiting the viewport. The underline bar's ::after pseudo-element then references that exact same name via animation-timeline: --para-read, even though it is a sibling element, not the paragraph itself and not a descendant of it. CSS scroll-driven animations resolve a named timeline by searching up the accessible tree for the nearest element or ancestor exposing that name, which is what allows one element's scroll position to drive a completely different element's animation.

Why this is more honest than animating the underline's own timeline

If view-timeline-name were declared on the underline bar instead, the fill would track the underline's own two-pixel-tall transit through the viewport — which happens almost instantly, since it is a thin line, not a real "reading progress" signal. Sourcing the timeline from the full paragraph instead means the fill genuinely represents how much of that block of prose has scrolled past the reader, which is the actual semantic the effect is named for.

animation-range narrows it to the readable window

animation-range: cover 0% cover 85% completes the fill slightly before the paragraph's absolute last pixel leaves the viewport, so the underline reaches 100% right around when a reader has plausibly finished reading the passage rather than only at the exact geometric edge case of the last line scrolling out.

Comparing to a document-wide reading bar

CSS Scroll-Driven Progress Bar and Scroll Reading Time both represent progress through the *entire* document. This underline is deliberately scoped — three separate underlines in this demo each track their own paragraph independently, useful for structured content like a FAQ or a stepped explainer where "progress through this specific answer" matters more than "progress through the whole page."

Browser support

Chromium-based browsers (Chrome, Edge, Opera, Brave) support named view() timelines referenced across sibling elements today. Firefox and Safari support is still landing, so an @supports not (animation-timeline: view()) block shows every underline fully filled rather than stuck empty.

Customizing it

Move view-timeline-name onto a different, more meaningful element (a whole <section> instead of a single paragraph) to change what "read" means, swap scaleX for a width change to a percentage-based fill instead, or add a small percentage label using the same --para-read timeline for a numeric readout beside each underline. Pair it with a FAQ Search Accordion for a "how much of this answer have you read" cue on long answers.

Step by step

How to Use

  1. 1
    Paste the HTML, CSS, and JSThree headings, each with an underline and a paragraph, render immediately.
  2. 2
    Scroll past each paragraphWatch the underline beneath its heading fill left-to-right as you pass through that paragraph.
  3. 3
    Scroll back upEach underline empties again in reverse, since it is driven by a live view timeline.
  4. 4
    Change what counts as "read"Move view-timeline-name from .hup-source to a wrapping section to track a larger block of content instead.
  5. 5
    Adjust the completion pointTune animation-range: cover 0% cover 85% to make the fill finish earlier or later relative to the paragraph's exit.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Long-form articles with distinct sub-sections
Give each subsection its own legible read-progress cue instead of one document-wide bar like CSS Scroll-Driven Progress Bar.
FAQ and documentation answers
Show how much of a specific long answer remains inside a FAQ Search Accordion or docs page.
Learn cross-element named view timelines
A focused demo of view-timeline-name resolving across sibling elements, not just an element animating itself.
Stepped explainers and tutorials
Pair with a Scroll Pin Steps sequence so each step shows its own read progress.
Replace a per-paragraph IntersectionObserver ratio tracker
Removes the need for scroll-listener-based intersectionRatio math for this specific per-passage effect.
Related: Scroll Reading Time
See the Scroll Reading Time for a document-wide reading progress alternative.
Related: Direction-Aware Grid Reveal (IntersectionObserver)
See the Direction-Aware Grid Reveal (IntersectionObserver) for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

view-timeline-name declares a named timeline sourced from the element it is set on (the paragraph, here). Any other element can then bind to that exact name via animation-timeline, as long as it is within the accessible scope of the name — the browser resolves the named timeline the same way it would resolve a custom property, letting one element's scroll-driven visibility control a completely different element's animation.

The underline bar is only a few pixels tall, so its own transit through the viewport happens almost instantly and would make the fill snap nearly all at once rather than tracking genuine reading progress. Sourcing the timeline from the full paragraph instead means the fill duration matches how long that block of text actually takes to scroll past.

It completes the fill once the paragraph is 85% through its own cover range rather than the full 100%, so the underline reaches full width slightly before the very last pixel of the paragraph leaves the viewport — closer to when a reader has plausibly finished the passage than the exact geometric edge case.

Only its own paragraph. Each .hup-source paragraph declares its own view-timeline-name, so the three underlines in this demo are entirely independent — scrolling past the first paragraph does not affect the second or third underline's fill state at all.

The @supports not (animation-timeline: view()) block sets every underline's fill to transform: scaleX(1) with the animation removed, so Firefox and Safari users see every underline fully filled by default rather than stuck empty or animating incorrectly.

Yes. Move the view-timeline-name and view-timeline-axis declarations from the .hup-source paragraph onto a wrapping element such as the .hup-section, and the underline will then fill based on that larger block's scroll transit instead of just the one paragraph's.