Scroll Progress — <mv-scroll-progress>
Reading progress indicator: a pure-CSS bar at the top of the page (scroll timeline), or a bar / ring element with a percentage that follows any scrolling container, with optional spring smoothing.
| Category | Scroll |
|---|---|
| Type | Web Component (<mv-scroll-progress>) |
| Status | stable |
| Keywords | progress, reading, scroll-timeline, ring, percentage, indicator |
When to use
- Long articles or docs should show how far the reader has scrolled with a thin bar at the top
- A scrollable panel, modal or chat log needs its own progress bar or percentage ring
- A floating ring with a percentage should double as a compact reading indicator in a corner
Avoid when
- Readers need section-level navigation and what they actually read, not just a scroll percentage → use Reading Map instead
- The progress reflects a task or upload rather than scroll position → use Progress instead
Install
node scripts/add.mjs scroll-progress --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["scroll-progress"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/scroll-progress/scroll-progress.js, components/scroll-progress/scroll-progress.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="mv-sp-demo">
<style>
#mv-sp-demo { display: flex; flex-wrap: wrap; gap: 1rem; width: min(100%, 50rem); margin-inline: auto; align-items: flex-start; }
#mv-sp-demo .box { flex: 1 1 22rem; min-width: 0; }
#mv-sp-demo aside { flex: 1 1 13rem; }
#mv-sp-demo .box { height: 400px; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: var(--mv-bg); }
#mv-sp-demo .head { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 1rem; padding: .8rem 1rem .8rem 1.5rem; background: var(--mv-bg); border-bottom: 1px solid var(--mv-border); }
#mv-sp-demo .head b { display: block; font-size: .9rem; }
#mv-sp-demo .head > div:first-child { flex: 1; min-width: 0; }
#mv-sp-demo .head span { font-size: .75rem; color: var(--mv-fg-muted); }
#mv-sp-demo .head .mv-scroll-progress { position: absolute; top: auto; bottom: -1px; left: 0; right: 0; }
#mv-sp-demo article { padding: 1.25rem 1.5rem 2rem; line-height: 1.65; font-size: .92rem; color: var(--mv-fg-muted); }
#mv-sp-demo article h3 { margin: 1.5rem 0 .4rem; color: var(--mv-fg); font-size: 1.05rem; letter-spacing: -.01em; }
#mv-sp-demo article p { margin: 0 0 .8rem; }
#mv-sp-demo aside { display: grid; gap: .75rem; }
#mv-sp-demo .card { padding: 1rem 1.1rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-surface); }
#mv-sp-demo .row { display: flex; align-items: center; gap: .9rem; }
#mv-sp-demo small { display: block; color: var(--mv-fg-muted); font-size: .75rem; }
#mv-sp-demo code { font: 500 .7rem var(--mv-font-mono); color: var(--mv-fg-subtle); }
</style>
<div class="box mv-scroll-area" id="mv-sp-article" data-orientation="vertical" tabindex="0" aria-label="Article: scroll-driven animations">
<header class="head">
<div>
<b>Scroll-driven animations</b>
<span>Guide · 6 min read</span>
</div>
<mv-scroll-progress variant="circle" style="--mv-scroll-progress-size:2.6rem"></mv-scroll-progress>
<div class="mv-scroll-progress" data-scope="nearest"></div>
</header>
<article>
<p>For years, animating an element in step with scrolling meant listening to the <em>scroll</em> event, reading positions and writing styles on every frame. The result was often janky, especially on mobile.</p>
<h3>A native timeline</h3>
<p>With <code>animation-timeline: scroll()</code>, the browser ties a CSS animation directly to the scroll position. No JavaScript needed: the animation moves forward as you scroll down and rewinds as you scroll back up.</p>
<p>The bar at the top of this article is the simplest example: a single class, not one line of script.</p>
<p>You win twice: the animation runs off the main thread when possible, and the code disappears. Less computation, fewer bugs, and smoothness that no longer depends on how busy the page is.</p>
<h3>What about other browsers?</h3>
<p>Where the feature is still missing, the component falls back to a passive listener, throttled to one update per frame. The result looks the same; only the mechanics change.</p>
<h3>Any container</h3>
<p>The ring in the header follows its nearest container; the ones next to it target this exact article through the <code>scroller</code> attribute, wherever they sit on the page. The percentage is computed in CSS with a counter.</p>
<h3>Accessibility</h3>
<p>The indicator is purely decorative: the native scrollbar remains the reference for assistive technologies. With reduced motion, smoothing is turned off and the value tracks the position exactly.</p>
<p>Finally, remember to test your effects with the “reduce motion” setting turned on: content must stay complete and readable, with no information lost.</p>
<p>Happy reading — you made it to the end.</p>
</article>
</div>
<aside>
<div class="card row">
<mv-scroll-progress variant="circle" scroller="#mv-sp-article" smooth></mv-scroll-progress>
<div><b style="font-size:.9rem">Elsewhere on the page</b><small>Follows the article via <code>scroller</code></small></div>
</div>
<div class="card">
<small style="margin-bottom:.6rem">Smoothed bar (spring)</small>
<mv-scroll-progress scroller="#mv-sp-article" smooth style="--mv-scroll-progress-size:6px;--mv-scroll-progress-track:var(--mv-bg-muted)"></mv-scroll-progress>
<p style="margin:.7rem 0 0"><code>scroller="#mv-sp-article" smooth</code></p>
</div>
</aside>
</div>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
variant | bar | circle | bar | Horizontal bar or ring with the percentage in the center. |
scroller | selector | window | Tracked container, placed anywhere on the page. Without the attribute: the nearest scrolling container (or the page) via a native CSS timeline. | |
smooth | boolean | Spring smoothing (JS path only, disabled with reduced motion). | |
fixed | boolean | Bar fixed at the top of the screen; ring fixed at the bottom right. | |
fallback | boolean | Forces the JavaScript fallback (tests, or browsers without animation-timeline). | |
data-scope | nearest | On the .mv-scroll-progress class: follows the parent scrolling container (stuck to the top) instead of the page. |
Properties
| Name | Type | Description |
|---|---|---|
value | number | Progress 0 → 1 (JS path). |
CSS classes
| Name | Description |
|---|---|
mv-scroll-progress | Pure CSS: bar fixed at the top of the page, driven by scroll(root). Invisible without support. |
mv-scroll-progress-fill / -ring / -track / -value | Parts generated by the element. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-scroll-progress | Progress 0 → 1, written by the timeline or the JS (read-only, usable in your own styles). | |
--mv-scroll-progress-color | accent gradient | Bar background (color or gradient). |
--mv-scroll-progress-size | 3px / 3rem | Bar thickness or ring diameter. |
--mv-scroll-progress-track | transparent / var(--mv-border) | Track color. |
--mv-scroll-progress-ring | var(--mv-accent) | Ring color. |
--mv-scroll-progress-stroke | 3 | Ring thickness (in units of the 40 viewBox). |
Accessibility
Decorative (aria-hidden, pointer-events: none): the native scrollbar remains the reference for assistive technologies. Passive listeners throttled to one frame. Reduced motion: no smoothing.