webdevpuneet.com

ScrollOut Scroll Progress Bar — CSS Custom Property Snippet

ScrollOut Scroll Progress Bar · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

Requires
<div class="spb-bar-track"><div class="spb-bar-fill" id="spbFill"></div></div>
<div class="spb-badge" id="spbBadge">0%</div>
<div class="spb-page">
  <div class="spb-head">
    <span class="spb-tag">ScrollOut · CSS custom property</span>
    <h2>Scroll progress, no manual math</h2>
    <p>The bar's width is driven entirely by <code>--scroll-percent-y</code>, a CSS variable ScrollOut writes to the document element on every scroll tick.</p>
  </div>
  <article class="spb-copy">
    <h3>Section one</h3>
    <p>ScrollOut's cssProps option decorates the scrolling element with a set of custom properties describing overall scroll position. --scroll-percent-y is one of them: a 0-to-1 float representing how far down the page has been scrolled.</p>
    <h3>Section two</h3>
    <p>The bar itself never runs any JS math for its width. A single CSS rule reads the variable directly: width: calc(var(--scroll-percent-y, 0) * 100%).</p>
    <h3>Section three</h3>
    <p>A small badge in the corner mirrors the same percentage as text, read from the variable in JS only for that one display purpose.</p>
    <h3>Section four</h3>
    <p>Keep scrolling — the bar and badge track continuously, not in discrete steps, because the underlying scroll listener fires on every frame ScrollOut samples.</p>
    <h3>Section five</h3>
    <p>Near the bottom now. The bar should read close to 100% by the time this paragraph is in view.</p>
    <h3>End</h3>
    <p>That's the whole mechanism — one ScrollOut call, one CSS variable, one calc().</p>
  </article>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), 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

Zero manual scroll math
No scrollY / scrollHeight arithmetic anywhere in this snippet's JS.
Real documented cssProps option
scrollPercentY is an actual ScrollOut v2 variable, converted to --scroll-percent-y in kebab case.
CSS-only bar width
The progress fill reads the variable directly via calc(), with no inline style writes from JS.
Opt-in variable selection
Passing an object to cssProps enables only the named variables instead of the full set.
Fallback default value
A :root default keeps calc() valid before ScrollOut's first update.
Text mirror via getComputedStyle
The one legitimate case where JS must read the variable back, since CSS can't render it as text.
Smooth continuous updates
The bar tracks scroll position continuously rather than jumping in discrete steps.
Inheritable custom property
The variable is set once on the document root and consumed anywhere via normal CSS inheritance.

About this UI Snippet

ScrollOut Scroll Progress Bar — Reading a Real Documented cssProps Variable

Screenshot of the ScrollOut Scroll Progress Bar snippet rendered live

A scroll progress bar is usually built by listening to scroll, dividing window.scrollY by document.body.scrollHeight - window.innerHeight, and writing the result to a style property on every event. ScrollOut's cssProps option removes that math entirely by computing it internally and exposing the result as a CSS custom property you read with calc().

The real, documented option: cssProps

js ScrollOut({ cssProps: { scrollPercentY: true } });

cssProps is opt-in: passing true enables every variable ScrollOut can produce, and passing an object (as here) enables only the ones you name, keyed in camelCase. ScrollOut converts each key to a kebab-case CSS custom property when writing it to the DOM, so scrollPercentY: true becomes --scroll-percent-y on the tracked scrolling element — the document root by default, since no scrollingElement override was passed. It is a real ScrollOut v2 option name, not an invented one; the library's other scroll-position variables follow the same camelCase-to-kebab convention (--scroll-percent-x, --scroll-dir-y, and so on for the per-target visibility props like --visible-y).

Why the bar itself has no JavaScript

css .spb-bar-fill{width:calc(var(--scroll-percent-y,0) * 100%)}

--scroll-percent-y is already a 0-to-1 float, so multiplying by 100% inside calc() gives a percentage the width property can consume directly. Because ScrollOut updates this variable on the *document element* rather than on the bar itself, and CSS custom properties inherit down through the DOM tree, the rule above just works without JS ever touching .spb-bar-fill's style — the browser's own style-recalculation pipeline handles the redraw every time the variable's computed value changes, which is typically cheaper than a scroll-driven JS style write because it skips the extra JS-to-style round trip on every tick.

Why the badge still needs one JS read

CSS has no way to render a custom property's numeric value as visible text — content: var(--scroll-percent-y) doesn't work the way you'd hope, because content treats it as an opaque string, not a computable percentage. So the small corner badge is the one piece of this snippet that reads the variable back out with getComputedStyle(root).getPropertyValue('--scroll-percent-y'), parses it as a float, and writes formatted text — a narrow, deliberate exception to the "let CSS own it" rule, done in a requestAnimationFrame loop so the number stays in sync with the bar without a redundant scroll listener of its own.

:root{ --scroll-percent-y: 0 } as a fallback

The variable is defined with a default value at the top of the stylesheet so the calc() in .spb-bar-fill has something valid to read before ScrollOut's first scroll sample runs — without it, an unset custom property makes the whole calc() expression invalid and the bar would render at its browser-default width instead of 0 on first paint.

Step by step

How to Use

  1. 1
    Add the ScrollOut CDN scriptOne script tag — no companion stylesheet is needed for this snippet.
  2. 2
    Enable cssProps.scrollPercentYPass ScrollOut({ cssProps: { scrollPercentY: true } }) to opt into just that one variable.
  3. 3
    Read the variable in CSS with calc()width: calc(var(--scroll-percent-y, 0) * 100%) on the bar — no JS style writes.
  4. 4
    Define a fallback default:root { --scroll-percent-y: 0 } keeps the calc() valid before the first scroll sample.
  5. 5
    Optionally mirror the value as textgetComputedStyle(root).getPropertyValue() is the only way to print the number, since CSS content can't render it.
  6. 6
    Scroll the pageThe bar fills smoothly from 0% to 100% as scroll position moves from top to bottom.

Real-world uses

Common Use Cases

Blog and article pages
Add a top progress bar to posts without any scroll arithmetic, since ScrollOut supplies --scroll-percent-y as a CSS variable.
CSS custom property teaching
Demonstrate JavaScript as a pure data supplier, with CSS reading the variable through calc() to set the bar width.
Docs reading progress
Pair with a table of contents to show reading position, enabling only the named variable through cssProps as an object.
Long landing pages
Give a subtle progress cue on a page with a lot of scroll, with no manual scrollY or scrollHeight maths anywhere in the code.

Got questions?

Frequently Asked Questions

Yes. It comes from ScrollOut's cssProps option, which decorates the tracked scrolling element with CSS custom properties describing scroll state. The JS key is camelCase (scrollPercentY) and ScrollOut writes it to the DOM as kebab-case (--scroll-percent-y), following the same conversion used for its other variables.

Passing true enables every CSS variable ScrollOut can produce, including several this page doesn't use. Passing { scrollPercentY: true } opts into only that one, which keeps unnecessary custom properties off the document element.

ScrollOut writes --scroll-percent-y on the document root on every scroll sample, and CSS custom properties inherit down the DOM tree. The bar's CSS rule, width: calc(var(--scroll-percent-y, 0) * 100%), reads that inherited value directly, so the browser's style engine handles the redraw without any JS style write.

CSS has no way to render a custom property's value as visible text — content: var(...) treats it as an opaque token, not a computed percentage. Reading it back with getComputedStyle() and writing formatted text is the only way to display the number, so the badge is a deliberate, narrow exception.

It gives the variable a valid default before ScrollOut writes its first real value. Without a default, an unset custom property makes the calc() expression that depends on it invalid, and the bar would fall back to its default CSS width instead of starting at 0%.

Yes, with one change — pass scrollingElement pointing at that container to ScrollOut's config, since --scroll-percent-y is written to whichever element ScrollOut is told is the scrolling element, which defaults to the document.