Scroll✨ Premium

Text Highlight Scroll

Thirty-three words at 15% opacity in a 280 px box that scrolls over 620 px; on every scroll event of the container, the shipped listener lights floor(ratio × 33) words in #e0e7ff, turns the next one violet and moves a 3 px bar along — text and gauge sit in a sticky block so they stay in view up to the last word.

JSTextHighlight

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Scroll has 70 effects, including 12 free. Explore the category →

3 usage examples

Headline quote of a sustainability report — manifesto of a B Corp company — Text Highlight Scroll example 1

① Headline quote of a sustainability report — manifesto of a B Corp company

When"Our commitment" page of a statically generated annual report (Hugo, Next.js export): a full-width block, the manifesto sentence at 21 px, a reading gauge underneath.
WhyThe box is self-contained: it scrolls inside its own frame (overflow-y: auto), independently of the page's scroll — a reading module you drop into the middle of a long report without touching the rest. The shipped listener (an ES5 IIFE of about twenty lines) pastes inline into the template, with no bundler or library, and the progress bar shows how much of the manifesto has been read. At 31 words over 620 px, a word lights up every 20 px, and the pinned block keeps the sentence in view up to the last one.
SettingsWords at 21 px / 600, unread rgba(238,247,240,.16), read #eef7f0, current #8ff0b4 (13:1 on #0b1a14), green gradient bar; padding of the pinned block .ss-highlight-pin brought down to 26px 32px 0; scroll-behavior: auto.
Values charter of an architecture studio — Philosophy page — Text Highlight Scroll example 2

② Values charter of an architecture studio — Philosophy page

When"Philosophy" page of a studio: three one-word principles (Rigour. Restraint. Permanence.) then a sentence, in a 27 px serif on a paper background.
WhyMath.floor(ratio × n) gives each word its own slice of scroll: 25 words over 620 px, one word every 24.8 px — reading becomes a gesture, and the three principles light up one by one, each in terracotta while it is "the current word". The light background shows that the effect comes down to three colors to change; the block (spans + CSS + listener) pastes into a CMS embed without depending on the rest of the page, and the script counts the spans it finds: 25 words or 33, nothing to configure.
SettingsGeorgia 27 px / 400, unread rgba(28,25,20,.16), read #1c1914, current #8f3a1a (6.6:1 on #f3efe7), bar #8f3a1a, hint and scrollbar recolored, top padding of the pinned block at 30 px.
Programme of a medical congress — the day's themes — Text Highlight Scroll example 3

③ Programme of a medical congress — the day's themes

WhenProgramme page of a congress published by the scientific committee from Markdown converted to HTML: a header with dates and venue, the day's themes at 20 px, a gauge.
WhyA single HTML file is enough: the spans, the stylesheet and the listener sit inline in the page, with no separate .js file to maintain and no pipeline. The current word in cyan (8.9:1 on #071021) acts as a reading cursor over the list "Precision oncology. Immunology. Bioethics."; the committee changes the themes by editing the spans, without touching the script, which reads the words from the HTML.
SettingsWords 20 px / 600, unread rgba(230,241,255,.15), read #e6f1ff, current #38bdf8, bar #0ea5e9 → #38bdf8, 25 words (one every 24.8 px), pinned block padding 26px 32px 0.

How it works

The sold HTML nests .demo-preview (background #0a0a0f, white text, sans-serif, position: relative: the block carries its own background and font, so it stays readable when pasted into a white page) → .sdc-scroll-container.ss-highlight-scroll (width: 100%, height: 280px, overflow-y: auto, scroll-behavior: smooth, 4 px WebKit scrollbar) → .ss-highlight-content (height: 900px) → .ss-highlight-pin (position: sticky, top: 0, padding: 40px 16px) → .ss-highlight-text#ssHighlightText (line-height: 2) holding 33 span.ss-highlight-word, then a 3 px .ss-highlight-progress-track (rgba(255,255,255,.1), margin-top: 16px) and its fill #ssHighlightFill (width: 0, #8b5cf6 → #d946ef gradient, transition: width .1s). Each word is a 1rem inline-block, font-weight: 600, in rgba(255,255,255,.15) with transition: color .2s; two state classes: .lit (#e0e7ff) and .accent (#a78bfa). Below the box, .sdc-scroll-hint "Scrollez ↓" (.65rem, opacity pulsing from .3 to .7 over 2 s, sdcHintPulse). Measured on a blank 800 px wide page: the paragraph takes 3 lines of 32 px, from 40 to 136 px below the top of the box; the track ends at 155 px; the pinned block is 195 px tall; the remaining 705 px of content are empty and only serve to provide 620 px of travel (900 − 280). At 1,280 px, 2 lines (64 px) and a 163 px block.

The listener is an ES5 IIFE (var, function, no global variable) that walks document.querySelectorAll('.ss-highlight-scroll'): each box finds its text and its fill inside itself (container.querySelector), so several boxes on the same page are animated independently. The words are the spans of the HTML — the script rebuilds nothing through innerHTML, so the whitespace between tags is kept (measured gap between two words: 4.45 px at 1rem) and you change the text by editing the spans. On every scroll event of the container (passive listener), and on resize: ratio = scrollTop / (scrollHeight − clientHeight), litCount = Math.floor(ratio × 33); words with index i < litCount get .lit, the one with i === litCount gets .accent (the word "being read", in violet), and fill.style.width = ratio × 100 + '%'. Measured: at 100 px of scroll, 5 words lit and the 6th in violet, bar at 16.1%; at 300 px, 15; at 600 px, 31; at 620 px, all 33 in #e0e7ff, no accent left, bar at 100%. One word every 18.8 px of scroll. The function is called once at initialisation: before any scrolling, no word is lit but the first one already carries the accent and the bar is at 0%.

Why the block is pinned. The paragraph is 96 px tall in a 280 px window with 620 px of travel: if it scrolled with the content, it would leave the frame entirely at scrollTop = 136 (ratio 0.219, 7 words out of 33) and the next 26 words would light up out of view. .ss-highlight-pin with position: sticky; top: 0 inside .ss-highlight-content settles this: measured, the text stays from 40 to 136 px below the top of the box at every position from 0 to 620 px, the track at 155 px. A sticky element never leaves its parent: the 195 px block can glide down to 900 − 195 = 705 px, beyond the 620 px of travel, so it never detaches. At 390 px wide, the paragraph grows to 6 lines (192 px) and the block to 291 px, taller than the window: at the end of travel it is pushed up by 11 px (text from 29 to 221 px), still entirely visible. Detail: the "Scrollez ↓" hint is positioned relative to .demo-preview (bottom: 8px) and overlaps the last 10 px of the box.

Accessibility

  • prefers-reduced-motion handled in the CSS: @media (prefers-reduced-motion: reduce) { .sdc-scroll-hint { animation: none } } stops the pulsing of the "Scrollez ↓" hint (sdcHintPulse, opacity .3 ↔ .7 every 2 s, infinite), the effect's only autonomous animation. The highlight is driven by the user's own scrolling, so there is no vestibular risk; the color (.2 s) and width (.1 s) transitions stay active.
  • Contrast measured on #0a0a0f: unread words rgba(255,255,255,.15) = #2f2f33, 1.48:1 — unreadable by design until you scroll; read words #e0e7ff 16.0:1; current word #a78bfa 7.3:1; bar 4.7 to 5.7:1; "Scrollez ↓" hint 2.6:1 at 10.4 px. If the sentence carries information (a manifesto, a programme), raise the base opacity to .45 (4.5:1): the effect remains a progressive emphasis and stops being a reveal.
  • Screen readers: the 33 spans are ordinary inline text, read as one sentence whatever the scroll position — do not put aria-hidden on them. Do put it on the "Scrollez ↓" hint and on the progress track (a div with no role), or give the track role="progressbar" with aria-valuenow updated in the listener's update function.
  • Keyboard: the overflow-y: auto box has no tabindex. Chrome 130+ makes scrollable regions without focusable content focusable, not every browser does: add tabindex="0" and an aria-label to the container so arrow keys and Space scroll it. Touch: one-finger scrolling of a nested overflow-y: auto box works on iOS and Android and does fire scroll events; at the end of travel, wheel and finger hand over to the page (no overscroll-behavior is set).

Browser compatibility

ES5 (var, function, Array.prototype.forEach.call on the NodeList), classList, scroll event on an overflow-y: auto container; on the CSS side, position: sticky, transition, linear-gradient, scroll-behavior (optional) and ::-webkit-scrollbar (WebKit/Blink only, Firefox keeps its native bar). No animation-timeline: unlike the 45 effects of this category driven by a CSS timeline (fx-0545, fx-0546, fx-0592…), which Firefox reads in no version, this highlight works in Firefox. No dependency: no library, no image, no hosted file — the three pasted blocks are enough.

Chrome 56+✓ Full (unprefixed sticky)
Firefox 32+✓ Full (native scrollbar)
Safari 13+✓ Full (6.1 to 12 with -webkit-sticky)
Edge 16+✓ Full
Mobile iOS 13+✓ Full (touch scrolling of the box)
Android Chrome✓ Full (touch scrolling of the box)

Without JavaScript: a paragraph at 15% opacity, pinned, in a 280 px box that scrolls over nothing, an empty bar, a pulsing hint. Without position: sticky, the block scrolls with the content and leaves the frame at 136 px while the words keep lighting up. Without scroll-behavior, programmatic scrolling is instant; without ::-webkit-scrollbar, the bar is the system's.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="sdc-scroll-container ss-highlight-scroll">
  <div class="ss-highlight-content">
    <div class="ss-highlight-pin">
      <div class="ss-highlight-text" id="ssHighlightText">
        <span class="ss-highlight-word">Le </span>
        <span class="ss-highlight-word">scrollytelling </span>
        <span class="ss-highlight-word">transforme </span>
        <span class="ss-highlight-word">… </span>
      </div>
      <div class="ss-highlight-progress-track">
        <div class="ss-highlight-progress-fill" id="ssHighlightFill"></div>
      </div>
    </div>
  </div>
</div>
<span class="sdc-scroll-hint">Scroll ↓</span>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Text (HTML — the span.ss-highlight-word elements) 33 words, one span per word The script counts the spans present in .ss-highlight-text at initialisation: 12 words or 80, the pace adapts (travel ÷ word count). Keep the trailing space inside each span or a line break between them: with inline-block, that is what separates the words (4.45 px at 1rem). Several .ss-highlight-scroll boxes on the same page are each animated by the same IIFE.
Pinned block (CSS — .ss-highlight-pin position / padding) sticky; top: 0; padding 40px 16px Keeps text and track in the frame for the whole travel: measured, the paragraph stays from 40 to 136 px below the top of the box from 0 to 620 px of scroll. position: static restores the behavior of a paragraph scrolling with its content (it leaves the frame at 136 px while 7 words are lit). The padding sets where the text sits in the window.
Content height (CSS — .ss-highlight-content height) 900px (620 px of travel) Travel = height − 280 px. It sets the pace: 620 / 33 = 18.8 px per word. 500px (220 px of travel, 6.7 px per word) for a one-flick read; 1400px for a slow read. The pinned block (195 px at 800 px wide) stays attached as long as it is shorter than the 280 px window, whatever the content height.
Box height (CSS — .sdc-scroll-container height) 280px Visible window. Taller, the travel shrinks for the same content (900 − height). Since the text is pinned, the height only needs to hold the paragraph and the track: 195 px at 800 px wide, 291 px at 390 px (beyond the window, the block is pushed up by the difference at the end of travel — 11 px, without hiding the text).
Word colors (CSS — .ss-highlight-word, .lit, .accent) rgba(255,255,255,.15) / #e0e7ff / #a78bfa Three states. The base opacity sets readability before reading (1.48:1 at .15, 4.5:1 at .45). Only one word carries the accent at a time — the first one from load; after the last word, no accent remains.
Color transition (CSS — .ss-highlight-word transition) color .2s Fade of each word to .lit / .accent. At 0, the lighting follows the scroll frame by frame; at .5s, a trail: several words fading at once after a wheel flick.
Typography (CSS — .ss-highlight-word font-size / font-weight, .ss-highlight-text line-height) 1rem / 600 / 2 inline-block on each word: words never break at the end of a line and the spaces come from the HTML between the spans (4.45 px at 1rem), which the script preserves since it does not rewrite the HTML. A line-height of 1.5 to 1.6 is enough at large sizes.
Progress bar (CSS — .ss-highlight-progress-fill) #8b5cf6 → #d946ef gradient, 3 px, transition width .1s Width written in % by the listener. Remove track and fill if you do not want them: the listener checks if (fillEl) before writing.
Smooth scrolling (CSS — .sdc-scroll-container scroll-behavior) smooth Applies to programmatic jumps (scrollTo, anchors): the words then light up one by one during the glide. Switch to auto if you drive scrollTop yourself frame by frame.

FAQ

Three things. The script is an IIFE that runs as soon as it is read and looks for the .ss-highlight-scroll boxes already present: place it after the HTML (end of body), or load it with defer if it lives in the head. The words must be span.ss-highlight-word elements inside .ss-highlight-text: that is what the script counts. And the box must actually scroll — 900 px of content in a 280 px window; if you shrank .ss-highlight-content below the box height, there is no travel left and the ratio stays at 0. Measured on a blank page with the three blocks pasted as they are: at 100 px of mouse wheel, 5 words lit and the 6th in violet, no console error.
Yes: text and track sit in .ss-highlight-pin, a position: sticky; top: 0 block inside 900 px of content. The box scrolls (the scrollbar and the gauge show it), the paragraph stays from 40 to 136 px below the top and the words light up under your eyes — without that, a 96 px paragraph leaves a 280 px window at 136 px of scroll, 7 words lit out of 33, and the next 26 would light up out of view. For text that scrolls with its content, set .ss-highlight-pin { position: static } and write a paragraph long enough to fill the travel, or bring the content height down to what the text occupies.
Same floor(ratio × n) formula in a 280 px container, with four differences. fx-0579 targets a single container by id in ES2015 (arrow functions) and only acts on the first scroll; fx-0591 is an ES5 IIFE that animates every .ss-highlight-scroll box on the page and accents the first word from load. fx-0591 adds the progress bar and pins the text; fx-0579 has neither gauge nor pinning. fx-0579 uses display: inline (.3 s transition, read words in #fff, current #a855f7) on 64 words; fx-0591, inline-block, .2 s, #e0e7ff / #a78bfa on 33 words. Finally fx-0579's text fills its box and scrolls with it; fx-0591's stays in place. Take fx-0579 for a long paragraph read by scrolling; fx-0591 for a short sentence that stays in view, with a gauge.