GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Scroll Inertia CSS — 9 Value Patterns

One value keeps its last scroll speed and shrinks to zero after stopping, one stretches past the boundary and springs back on release, and one reverses

Auto-plays · click a tile to jump to its section · all nine in one zip

All nine react to real scroll input and nothing else: eight (01–05, 07–09) answer both a real wheel event and a pointer drag (pointerdown/pointermove/pointerup), and the ninth (06) answers a real scroll event instead, guarded by its own autoScrolling flag so its own programmatic auto-loop never mistakes itself for a visitor's scroll. 06, 08, and 09 also answer a real keydown for arrow-key scrolling, and 01 and 03 re-measure their scrollable height on resize. None of the nine calculates its motion from Math.random — every curve above comes from a deterministic requestAnimationFrame loop reading fixed constants, so the same scroll input always produces the same numbers, frame for frame. That's the kind of scroll interaction that website builders reach for when a hero header or a sticky nav needs to feel driven by real scroll physics rather than a canned CSS transition.

Anatomy of nine patterns — which value changes and when

Why five patterns decay a value by a fixed ratio every frame instead of integrating a force

01, 03, 05, 07, and 08 never keep a separate acceleration value — each one multiplies (or eases) a velocity or position by one fixed ratio constant every frame, so the value's own last state is the only input the next frame needs: vel *= FRICTION (01), vel *= COAST_FRICTION (03), vel *= L.decay per layer (05), vel += (BASE_SPEED - vel) * FRICTION_PULL (07), and thumbY += (target - thumbY) * LAG (08).

Why three patterns integrate a spring's restoring force into a velocity into a position every frame

02, 04, and 09 each compute a restoring force from how far a value sits from its resting value (force = -SPRING_K * (x - target) - SPRING_C * v), add that force into a velocity, and add the velocity into the value itself, every single frame. That's why all three can overshoot their own resting value and swing back — 02's header stretch, 04's section position, and 09's nav scale all pass through the target before settling rather than easing straight there.

Why one pattern integrates a constant gravity acceleration instead of a spring's restoring force

06 is the only one of the nine that adds a constant (GRAVITY) to its velocity every frame regardless of position, the way a falling object accelerates at a fixed rate rather than being pulled harder the farther it strays from a target. Only when its position value reaches FLOOR_Y does 06 reverse and shrink that velocity by RESTITUTION, which is what turns one continuous fall into a bouncing settle instead of a single spring swing.

Code per pattern

01Scroll inertia coast — the position value keeps carrying the last scroll speed and gradually shrinks to zero after scrolling stops

The position value inherits the speed at release and keeps shrinking until it comes to rest. A long list page or an image gallery scroll reads this way, since the glide keeps moving in the same direction the visitor was already scrolling instead of stopping the instant a finger or wheel lets go.

Scroll inertia coast — the position value keeps carrying the last scroll speed and gradually shrinks to zero after scrolling stops
Open in a new tab

01 · Scroll inertia coast — the position value keeps carrying the last scroll speed and gradually shrinks to zero after scrolling stops — Measured area 20.972% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

FRICTIONMIN_VELOCITYWHEEL_SCALE

What the numbers say

Measured across 23 tracked frames, 01 moves on 20 of them, spanning a 20.972% cumulative area at a 94.7 average intensity — one of the largest areas in this collection, since a coasting scroll keeps the whole track sliding rather than shifting one small part of it.

  var WHEEL_SCALE = 0.55;

  var pos = 0, vel = 0, raf = null, maxScroll = 0;

  function measure() {
    maxScroll = Math.max(0, track.scrollHeight - viewport.clientHeight);
  }
  function clamp(v) { return Math.max(-maxScroll, Math.min(0, v)); }
  function paint() { track.style.transform = 'translateY(' + pos.toFixed(1) + 'px)'; }

  function stopCoast() { if (raf !== null) { cancelAnimationFrame(raf); raf = null; } }
  function coastTick() {
    vel *= FRICTION;
    pos = clamp(pos + vel);
    if (pos === 0 || pos === -maxScroll) { vel = 0; }
    paint();
    if (Math.abs(vel) < MIN_VELOCITY) { raf = null; return; }
    raf = requestAnimationFrame(coastTick);
  }
  function startCoast(v) {
    vel = v;
    if (reduce) { pos = clamp(pos + vel * 6); vel = 0; paint(); return; }
    stopCoast();
    raf = requestAnimationFrame(coastTick);
  }

02Overscroll elasticity — past the scroll boundary, a stretched value springs back

Pulling past the edge stretches a value that springs back to its original value on release. A mobile top header or an app-style landing page reads this way, since the header visibly stretching past the boundary — then springing back — signals the scroll has reached its real edge rather than just stopping dead.

Overscroll elasticity — past the scroll boundary, a stretched value springs back
Open in a new tab

02 · Overscroll elasticity — past the scroll boundary, a stretched value springs back — Measured area 21.332% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PULL_RESISTSPRING_KSPRING_C

What the numbers say

Measured across 23 tracked frames, 02 moves on all 23 of them, spanning a 21.332% cumulative area at a 65.3 average intensity — the header keeps easing back through its own overshoot on every single tracked frame rather than settling early.

  function springTick() {
    var force = -SPRING_K * (stretch - 1) - SPRING_C * sVel;
    sVel += force * (1 / 60) * 0.06;
    stretch += sVel;
    paint();
    if (Math.abs(stretch - 1) < 0.003 && Math.abs(sVel) < 0.003) {
      stretch = 1; sVel = 0; paint(); raf = null; return;
    }
    raf = requestAnimationFrame(springTick);
  }
  function release() {
    if (reduce) { stretch = 1; sVel = 0; paint(); return; }
    stopSpring();
    raf = requestAnimationFrame(springTick);
  }
  function pullTo(dist) {
    var d = Math.max(0, dist);
    stretch = Math.min(MAX_STRETCH, 1 + Math.sqrt(d) / PULL_RESIST);
    paint();
  }

03Scroll velocity blur — a blur value grows in proportion to the measured scroll speed value

As the speed value rises the blur value grows, and it returns to zero once scrolling stops. A gallery background image or a hero section background reads this way — the same horizontal scroll interaction that moves the track also drives how blurred it looks, so a fast flick reads as motion blur rather than a static image sliding past.

Scroll velocity blur — a blur value grows in proportion to the measured scroll speed value
Open in a new tab

03 · Scroll velocity blur — a blur value grows in proportion to the measured scroll speed value — Measured area 20.219% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

VELOCITY_TO_BLURMAX_BLURCOAST_FRICTION

What the numbers say

Measured across 23 tracked frames, 03 moves on 18 of them, spanning a 20.219% cumulative area at a 105.2 average intensity — the highest average intensity of the nine, since every tracked frame shifts either the track's position or its blur radius, often both at once.

  function paint() {
    track.style.transform = 'translateX(' + pos.toFixed(1) + 'px)';
    track.style.filter = blur > 0.05 ? 'blur(' + blur.toFixed(2) + 'px)' : 'none';
  }
  function stopLoop() { if (raf !== null) { cancelAnimationFrame(raf); raf = null; } }
  function loopTick() {
    vel *= COAST_FRICTION;
    pos = clamp(pos + vel);
    if (pos === 0 || pos === -maxScroll) { vel = 0; }
    var target = Math.min(MAX_BLUR, Math.abs(vel) * VELOCITY_TO_BLUR);
    blur += (target - blur) * BLUR_SMOOTH;
    paint();
    if (Math.abs(vel) < MIN_VELOCITY && blur < 0.05) { blur = 0; paint(); raf = null; return; }
    raf = requestAnimationFrame(loopTick);
  }
  function startLoop(v) {
    vel = v;
    if (reduce) { pos = clamp(pos + vel * 6); vel = 0; blur = 0; paint(); return; }
    stopLoop();
    raf = requestAnimationFrame(loopTick);
  }

04Section snap spring — the released scroll position value settles toward the nearest section value through spring damping

The released position value oscillates around the nearest section value before settling. A full-page section layout or a slide-style intro page reads this way, since letting go anywhere mid-scroll still resolves into a clean stop at the nearest section rather than an arbitrary halfway position.

Section snap spring — the released scroll position value settles toward the nearest section value through spring damping
Open in a new tab

04 · Section snap spring — the released scroll position value settles toward the nearest section value through spring damping — Measured area 20.706% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SECTION_HSPRING_KSPRING_C

What the numbers say

Measured across 23 tracked frames, 04 moves on all 23 of them, spanning a 20.706% cumulative area at an 88.7 average intensity — the spring keeps correcting its own overshoot past the target section on nearly every frame before it settles.

  function clampFree(v) { return Math.max(-maxScroll, Math.min(0, v)); }
  function paint() { track.style.transform = 'translateY(' + pos.toFixed(1) + 'px)'; }
  function stopSpring() { if (raf !== null) { cancelAnimationFrame(raf); raf = null; } }
  function nearestTarget() { return clampFree(Math.round(pos / SECTION_H) * SECTION_H); }

  function springTick(target) {
    var force = -SPRING_K * (pos - target) - SPRING_C * vel;
    vel += force * (1 / 60) * 0.06;
    pos += vel;
    paint();
    if (Math.abs(pos - target) < 0.4 && Math.abs(vel) < 0.4) {
      pos = target; vel = 0; paint(); raf = null; return;
    }
    raf = requestAnimationFrame(function () { springTick(target); });
  }
  function snap() {
    var target = nearestTarget();
    if (reduce) { pos = target; vel = 0; paint(); return; }
    stopSpring();
    raf = requestAnimationFrame(function () { springTick(target); });
  }

05Per-layer decay parallax — after scrolling stops, each layer's position value keeps shrinking at its own decay rate

Each layer's own decay-rate value keeps its position value shrinking at a different pace after the stop. A hero background layer or a brand story page reads this way, since the layers visibly separate from each other the moment a drag ends instead of all stopping together as one flat image.

Per-layer decay parallax — after scrolling stops, each layer's position value keeps shrinking at its own decay rate
Open in a new tab

05 · Per-layer decay parallax — after scrolling stops, each layer's position value keeps shrinking at its own decay rate — Measured area 9.958% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

WRAPdecayspeed

What the numbers say

Measured across 23 tracked frames, 05 moves on all 23 of them, spanning a 9.958% cumulative area at an 83.6 average intensity — smaller than 01 through 04 because each layer only shifts by its own share of the drag, never the full width at once.

  function clampWrap(v) { var m = v % WRAP; if (m > 0) { m -= WRAP; } return m; }
  function paint() {
    for (var i = 0; i < layers.length; i++) {
      layers[i].el.style.transform = 'translateX(' + layers[i].pos.toFixed(1) + 'px)';
    }
  }
  function stopLoop() { if (raf !== null) { cancelAnimationFrame(raf); raf = null; } }
  var inputActive = false;
  function loopTick() {
    var moving = false;
    for (var i = 0; i < layers.length; i++) {
      var L = layers[i];
      if (!inputActive) { L.vel *= L.decay; if (reduce) { L.vel = 0; } }
      L.pos = clampWrap(L.pos + L.vel);
      if (Math.abs(L.vel) > 0.02) { moving = true; }
    }
    paint();
    if (reduce || (!moving && !inputActive)) { raf = null; return; }
    raf = requestAnimationFrame(loopTick);
  }
  function ensureLoop() { if (raf === null) { raf = requestAnimationFrame(loopTick); } }
  function setInput(v) {
    for (var i = 0; i < layers.length; i++) { layers[i].vel = v * layers[i].speed; }
    ensureLoop();
  }

06Gravity settle — on scroll entry, an element's position value falls under a gravity value and settles after bouncing off a floor value

The entering element's position value falls and bounces off a floor value repeatedly, each bounce smaller. A card list or a product grid section reads this way, since each card visibly drops into place and settles the moment it scrolls into view rather than simply fading in.

Gravity settle — on scroll entry, an element's position value falls under a gravity value and settles after bouncing off a floor value
Open in a new tab

06 · Gravity settle — on scroll entry, an element's position value falls under a gravity value and settles after bouncing off a floor value — Measured area 22.223% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRAVITYRESTITUTIONSTART_Y

What the numbers say

Measured across 23 tracked frames, 06 moves on 14 of them, spanning a 22.223% cumulative area at a 47.4 average intensity — the largest cumulative area of the nine, since a falling, bouncing card covers real vertical distance rather than a small local shift.

  function dropTick(card, y, vel) {
    vel += GRAVITY;
    y += vel;
    if (y >= FLOOR_Y) {
      y = FLOOR_Y;
      vel = -vel * RESTITUTION;
      if (Math.abs(vel) < STOP_VEL) {
        card.style.transform = 'translateY(0px)';
        card.dataset.done = '1';
        return;
      }
    }
    card.style.transform = 'translateY(' + y.toFixed(1) + 'px)';
    requestAnimationFrame(function () { dropTick(card, y, vel); });
  }
  function settle(card) {
    if (card.dataset.done) { return; }
    card.style.opacity = '1';
    if (reduce) { card.style.transform = 'translateY(0px)'; card.dataset.done = '1'; return; }
    card.dataset.done = '1';
    dropTick(card, START_Y, 0);
  }

07Friction decel — once the input value stops, a speed value slows gradually by a friction value

Once input ends, the speed value drops by a fixed friction value each step before returning to its base speed. A logo marquee or a news ticker banner reads this way, since the strip never fully stops on its own — dragging or spinning the wheel only ever shifts its speed value away from a steady baseline crawl.

Friction decel — once the input value stops, a speed value slows gradually by a friction value
Open in a new tab

07 · Friction decel — once the input value stops, a speed value slows gradually by a friction value — Measured area 5.444% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

BASE_SPEEDFRICTION_PULLCHIP_W

What the numbers say

Measured across 23 tracked frames, 07 moves on all 23 of them, spanning a 5.444% cumulative area at a 105.0 average intensity — the strip's own baseline crawl keeps every frame in motion even without any input at all.

  var CHIP_W = 56, GAP = 10, SET_COUNT = 6;
  var WRAP = SET_COUNT * (CHIP_W + GAP);
  var BASE_SPEED = -0.9;
  var FRICTION_PULL = 0.06;
  var pos = 0, vel = BASE_SPEED;
  var inputActive = false;
  var raf = null;

  function clampWrap(v) { var m = v % WRAP; if (m > 0) { m -= WRAP; } return m; }
  function paint() { track.style.transform = 'translateX(' + pos.toFixed(1) + 'px)'; }
  function loopTick() {
    if (reduce) { raf = null; return; }
    if (!inputActive) { vel += (BASE_SPEED - vel) * FRICTION_PULL; }
    pos = clampWrap(pos + vel);
    paint();
    raf = requestAnimationFrame(loopTick);
  }
  function ensureLoop() { if (raf === null && !reduce) { raf = requestAnimationFrame(loopTick); } }

08Lag tracking — a trailing value doesn't match the real scroll value instantly, chasing it with a lag value instead

A trailing value follows the real scroll value with a lag delay, then gradually catches up. A custom scrollbar or a document-style content page reads this way, since the handle visibly trails behind a fast scroll instead of locking to the exact same position every frame.

Lag tracking — a trailing value doesn't match the real scroll value instantly, chasing it with a lag value instead
Open in a new tab

08 · Lag tracking — a trailing value doesn't match the real scroll value instantly, chasing it with a lag value instead — Measured area 1.782% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

LAGSNAP_EPSrowH

What the numbers say

Measured across 23 tracked frames, 08 moves on 9 of them, spanning a 1.782% cumulative area at a 108.0 average intensity — the smallest area of the nine, since only the thumb itself ever moves, never the list or the track behind it.

  // BD19 — measure the rail and thumb's real pixels with getBoundingClientRect (never guess).
  var railRect = rail.getBoundingClientRect();
  var railH = railRect.height;
  var thumbH = Math.max(28, railH * (viewport.clientHeight / (rowH * rowCount)));
  thumb.style.height = thumbH.toFixed(1) + 'px';

  function paintTrack() { track.style.transform = 'translateY(-' + scrollPos.toFixed(1) + 'px)'; }
  function targetThumbY() { return maxScroll <= 0 ? 0 : (scrollPos / maxScroll) * (railH - thumbH); }
  function paintThumb() { thumb.style.transform = 'translateY(' + thumbY.toFixed(1) + 'px)'; }
  function loopTick() {
    var target = targetThumbY();
    if (reduce) { thumbY = target; paintThumb(); raf = null; return; }
    thumbY += (target - thumbY) * LAG;
    if (Math.abs(target - thumbY) < SNAP_EPS) {
      thumbY = target; paintThumb(); raf = null; return;
    }
    paintThumb();
    raf = requestAnimationFrame(loopTick);
  }

09Rubber pull — the moment scroll direction reverses, a value pulls like a rubber band and springs back

At the moment of reversal the value is pulled the opposite way, then springs back to its resting value. A top navigation bar or a mobile app-style header reads this way, since the bar only ever reacts at the instant a visitor changes their mind about scroll direction, staying still through any single steady scroll.

Rubber pull — the moment scroll direction reverses, a value pulls like a rubber band and springs back
Open in a new tab

09 · Rubber pull — the moment scroll direction reverses, a value pulls like a rubber band and springs back — Measured area 1.796% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

MIN_VELKICKSPRING_C

What the numbers say

Measured across 23 tracked frames, 09 moves on 9 of them, spanning a 1.796% cumulative area at a 44.3 average intensity — it only moves at all on the frames right after a direction reversal, then sits still again once the spring settles.

  function navTick() {
    var force = -SPRING_K * (navScale - 1) - SPRING_C * navVel;
    navVel += force * 0.016;
    navScale += navVel * 0.016;
    if (reduce) { navScale = 1; navVel = 0; paintNav(); navRaf = null; return; }
    if (Math.abs(navScale - 1) < 0.003 && Math.abs(navVel) < 0.02) {
      navScale = 1; navVel = 0; paintNav(); navRaf = null; return;
    }
    paintNav();
    navRaf = requestAnimationFrame(navTick);
  }
  function ensureNavLoop() { if (navRaf === null) { navRaf = requestAnimationFrame(navTick); } }
  function kickNav(sign) {
    if (reduce) { return; }
    navVel += sign * KICK;
    ensureNavLoop();
  }
  function applyVel(vel) {
    if (lastVel !== 0 && vel !== 0 && (lastVel > 0) !== (vel > 0) && Math.abs(lastVel) > MIN_VEL && Math.abs(vel) > MIN_VEL) {
      kickNav(vel > 0 ? 1 : -1);
    }
    if (Math.abs(vel) > 0.1) { lastVel = vel; }
  }

Where values reshape a pattern — decay, mass, and boundary criteria

Why 01's FRICTION constant decides how long the coast lasts, not how fast it starts

At FRICTION 0.93 the velocity value dies out within half a beat of release, so the list glides for only a moment; raise it to 0.99 and the very same release speed carries the list forward for several seconds, turning a short slide into a long inertial glide without changing how quickly the coast begins.

Why 06's RESTITUTION constant decides how many times a card rebounds, not how far it first falls

At RESTITUTION 0.42 a card bounces low once or twice off the floor value and stops there; raise it to 0.75 and the same drop from the same starting height rebounds high, several times over, before it finally settles — the first fall is identical either way.

Why 09's SPRING_K constant decides how many times the nav bar wobbles, not which direction it kicks

At SPRING_K 260 the nav bar snaps back once, short and stiff, on every direction reversal; lower it to 90 and the same kick makes the same spring wobble back and forth several times, softer each time, before it rests — which direction it kicks toward is decided elsewhere, by which way the reversal went.

These nine sit differently from three already-covered posts along one calculation axis: where the driving value actually comes from. 9 CSS Animation on Scroll Effects, No JavaScript (002) drives its motion straight from animation-timeline: view()/scroll() — a browser-computed progress ratio with no JS loop and no velocity value carried between frames at all, unlike these nine's own vel (or navVel, sVel) variable, read and rewritten every frame. Pull to Refresh CSS: 9 Loading Patterns (353) also reads a real dragged distance every frame, but several of its nine items check that distance against a threshold and commit an actual refresh once it's crossed — a state change beyond the screen that none of these nine ever makes, since a released value here only ever settles back into view. Button Press Animation UI: 9 Calculation Patterns (357) runs the same accel-into-vel-into-position spring loop as 02, 04, and 09 here, but every one of its nine loops resolves inside one bounded press-and-release, while a value here — 01's coasting vel, 05's per-layer decay, 07's pull toward BASE_SPEED — keeps answering an open-ended scroll session that never ends on its own. Grab the nine files as one archive to test a scroll inertia css tweak locally before committing to it — unzip it with the password azjysfxp and every constant named above sits right in the same index.html you just read.

Where it breaks — rapid scrolling, low-power devices, reduced motion

What happens when you reverse scroll direction mid-coast

01 and 03 both call stopCoast()/stopLoop() the instant a new wheel event arrives, then recompute a fresh velocity from only that event's deltaY (or deltaX) — a hard flick against a coast already running discards whatever speed value was still left rather than blending the two, so the value jumps onto a new curve instead of easing across the reversal.

What happens on a very long continuous scroll

None of the nine accumulates any state that grows with how long a visitor keeps scrolling — every velocity, position, and decay value is recomputed or clamped fresh from the current input each frame, so a scroll session lasting one second or one hour produces the exact same per-frame numbers for the exact same input.

What happens with prefers-reduced-motion on

Five of the nine (02, 04, 06, 08, 09) jump straight to an exact computed resting value the instant reduce is true — 1 for a stretch or scale value, a nearest section or lag target for a position value, or a flat resting spot for a settled card — never animating through it. 01 and 03 instead advance the position by a fixed six-frame approximation of the coast (vel * 6) and zero the velocity in the same step, which only estimates where a real coast would have ended rather than computing it exactly. 05 freezes in place the moment input stops, since it only zeroes each layer's velocity under reduce without ever computing a target to jump to, and 07 is the strictest of all — its ensureLoop() refuses to start the loop at all while reduce is true, so the marquee never moves from its starting position, not even in a single jump.

Three variants and accessibility

Item Constant Values What changes
01 inertia coast FRICTION 0.93 → 0.99 The same release speed still starts the coast, but the velocity value dies out in a half-beat versus carrying the list forward for several seconds — a short slide versus a long inertial glide
06 gravity settle RESTITUTION 0.42 → 0.75 The same drop from the same height still falls the same way, but the card stops after one or two low bounces versus rebounding high several times before it settles
09 rubber pull SPRING_K 260 → 90 The same direction reversal still kicks the same spring, but the nav bar snaps back once, stiff and short, versus wobbling back and forth softly several times before resting

All nine share the same role="application" and a descriptive aria-label naming what the panel is. 06, 07, 08, and 09 also carry tabindex="0" for keyboard focus, though only 06, 08, and 09 answer a real keydown — arrow-key scrolling for 06 and 08, a direction kick for 09 — while 07's tabindex makes it focusable without adding any key handling of its own. None of the nine adds an aria-live region anywhere, so every value change here stays a visual effect and is never announced; every item also checks prefers-reduced-motion: reduce the way the previous section describes, so a visitor who has turned it on in their OS never sees the loop run at all.

FAQ

Do any of these nine patterns need a physics library, or is plain JavaScript enough?

Plain JavaScript is enough — all nine run on an ordinary requestAnimationFrame loop doing arithmetic on a handful of numbers (a position, a velocity, a few constants); none of the nine imports a physics engine or anything beyond the browser's own scroll, pointer, and animation-frame APIs.

Is a scroll bounce effect css alone enough to build something like item 02 or 06?

Not on its own — CSS can ease a transform once a JavaScript value changes, but the actual bounce, overshoot, and settle in 02 and 06 come from a spring or gravity value recomputed every frame in JavaScript; CSS here only ever paints the number JavaScript hands it.

Why does item 06 trigger on scroll entry instead of on every scroll frame like the others?

Because it's wired to a real IntersectionObserver rather than a scroll listener — a card only starts falling the moment it crosses the 0.6 visibility threshold set on that observer, not on every pixel of scroll, which is also why 06 is the only one of the nine that never reruns once a card has already settled.

Sources: MDN — scroll event · MDN — overscroll-behavior · MDN — requestAnimationFrame · MDN — prefers-reduced-motion · MDN — IntersectionObserver

Enter the archive password

The password is inside this article. You will find it as you read.