Scroll Velocity Motion Blur — Free Real Velocity-Based CSS Blur Effect
Scroll Velocity Motion Blur · Scroll · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<section class="vb-intro"><h1>Scroll fast ↓</h1><p>Content genuinely blurs in proportion to how fast you're scrolling, then sharpens as you slow down.</p></section>
<section class="vb-list" id="vbList">
<article class="vb-card"><span>01</span><h3>Momentum</h3><p>Blur amount tracks real pixels-per-frame.</p></article>
<article class="vb-card"><span>02</span><h3>Inertia</h3><p>Fling the page and watch it smear.</p></article>
<article class="vb-card"><span>03</span><h3>Friction</h3><p>Slow down and it sharpens back to zero.</p></article>
<article class="vb-card"><span>04</span><h3>Drift</h3><p>No fixed blur toggle — it's computed live.</p></article>
<article class="vb-card"><span>05</span><h3>Glide</h3><p>Stop scrolling and blur decays to nothing.</p></article>
<article class="vb-card"><span>06</span><h3>Coast</h3><p>Try a slow, deliberate scroll versus a flick.</p></article>
</section>
<section class="vb-outro"><p>The blur amount is a live measurement, not a preset.</p></section>
<div class="vb-meter"><span class="vb-meter-label">px/frame</span><span class="vb-meter-value" id="vbMeterValue">0</span></div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0b13;color:#fff}
.vb-intro,.vb-outro{min-height:70vh;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;gap:10px;padding:24px}
.vb-intro h1{font-size:clamp(34px,7vw,64px);letter-spacing:-.02em}
.vb-intro p,.vb-outro p{color:#9aa0b8;font-size:16px;max-width:480px}
.vb-list{max-width:600px;margin:0 auto;padding:10vh 24px;display:flex;flex-direction:column;gap:20px;will-change:filter}
.vb-card{padding:26px 26px;border-radius:20px;background:linear-gradient(150deg,#181e34,#11131f);border:1px solid #252c40}
.vb-card span{font-size:13px;font-weight:800;letter-spacing:.14em;color:#7c8cff}
.vb-card h3{font-size:24px;letter-spacing:-.01em;margin:8px 0 6px}
.vb-card p{color:#a7adc4;font-size:14.5px;line-height:1.55}
.vb-meter{position:fixed;bottom:20px;right:20px;display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:999px;background:rgba(13,15,28,.85);backdrop-filter:blur(8px);border:1px solid #262d44;font-size:12.5px;z-index:5}
.vb-meter-label{color:#8a90a8;letter-spacing:.04em}
.vb-meter-value{font-variant-numeric:tabular-nums;font-weight:800;color:#22d3ee;min-width:2.4em;text-align:right}(function () {
var list = document.getElementById('vbList');
var meterValue = document.getElementById('vbMeterValue');
var lastScrollY = window.scrollY;
var lastTime = performance.now();
var velocity = 0; // measured pixels-per-frame
var currentBlur = 0;
var MAX_BLUR = 14;
// Smoothing/decay factors: velocity eases toward its freshly-measured
// value, and the applied blur eases toward the velocity-derived target,
// so both ramp up and settle down smoothly instead of snapping.
var VELOCITY_SMOOTHING = 0.35;
var BLUR_SMOOTHING = 0.18;
var DECAY_PER_FRAME = 0.9; // velocity decays toward 0 when no scroll event fires
var lastEventTime = performance.now();
window.addEventListener('scroll', function () {
var now = performance.now();
var dt = now - lastTime;
if (dt <= 0) return;
var dy = window.scrollY - lastScrollY;
// Normalize to a ~60fps frame so velocity reads as "pixels per frame"
// regardless of actual event timing.
var instVelocity = Math.abs(dy) / (dt / 16.67);
velocity += (instVelocity - velocity) * VELOCITY_SMOOTHING;
lastScrollY = window.scrollY;
lastTime = now;
lastEventTime = now;
}, { passive: true });
function loop() {
var now = performance.now();
// If no scroll event has fired recently, decay velocity toward zero so
// blur clears even if the scroll simply stops without a final event.
if (now - lastEventTime > 32) {
velocity *= DECAY_PER_FRAME;
if (velocity < 0.05) velocity = 0;
}
var targetBlur = Math.min(MAX_BLUR, velocity * 0.9);
currentBlur += (targetBlur - currentBlur) * BLUR_SMOOTHING;
if (currentBlur < 0.05) currentBlur = 0;
list.style.filter = currentBlur > 0 ? 'blur(' + currentBlur.toFixed(2) + 'px)' : 'none';
meterValue.textContent = velocity.toFixed(1);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();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.
What's included
Features
About this UI Snippet
Scroll Velocity Motion Blur — Blur That Tracks Real Scroll Speed

This isn't a fixed "blur while scrolling, sharp when still" toggle — the blur amount is a genuine, continuously computed function of how fast the page is actually moving. Scroll slowly and the content stays crisp; flick the page hard and it visibly smears with filter: blur(), then eases back to zero as your scroll speed drops. Built entirely with vanilla JavaScript and requestAnimationFrame, no animation library required.
Measuring real velocity, not just "is scrolling"
Every scroll event records the current window.scrollY, compares it against the value from the previous event, and divides by the elapsed time — normalized to a 16.67ms (60fps) frame so the result reads as "pixels moved per frame" regardless of how often the browser actually fires scroll events. That raw instantaneous velocity is smoothed with a simple exponential-moving-average step (velocity += (instVelocity - velocity) * 0.35) so a single noisy event doesn't spike the blur.
A second smoothing pass for the visual blur
The blur amount applied to the content doesn't jump straight to the velocity-derived target either — a requestAnimationFrame loop eases currentBlur toward targetBlur every frame with its own smoothing factor. Two smoothing stages (velocity, then blur) is what gives the effect a soft ramp-up and a soft settle rather than a jittery on/off flicker, while the underlying number driving it all is still a real speed measurement.
Decay when scrolling stops
Because scroll events stop firing entirely once the page is still, the rAF loop separately checks how long it's been since the last event and decays the velocity toward zero if none has arrived in the last couple of frames. Without this, a scroll that stops abruptly (rather than decelerating smoothly, as happens with certain trackpads or a scrollbar drag) would leave the blur stuck at its last value with nothing to bring it back down.
Live velocity readout
A small fixed meter in the corner displays the raw pixels-per-frame velocity number in real time, which makes the relationship between scroll speed and blur amount directly observable rather than something you have to take on faith — useful for tuning MAX_BLUR and the smoothing constants to taste.
Customizing it
Raise MAX_BLUR for a more dramatic smear, tighten BLUR_SMOOTHING for a snappier response, or apply the blur to individual cards instead of the whole list for a more localized effect. Pair it with scroll skew velocity for a combined blur-and-skew treatment, or contrast it with a scroll reveal grid for a calmer entrance.
Step by step
How to Use
- 1Paste HTML, CSS, and JSAn intro, a card list, a meter, and an outro render.
- 2Scroll slowlyThe meter stays low and the cards remain sharp.
- 3Flick-scroll fastThe meter spikes and the cards visibly blur.
- 4Stop suddenlyBlur eases back to zero even without a final scroll event.
- 5Watch the meterIt shows the live pixels-per-frame value driving the blur.
- 6Tune the responseAdjust MAX_BLUR and the smoothing constants to taste.
Real-world uses
Common Use Cases
rAF loop clearing blur even if no final scroll event fires.Got questions?
Frequently Asked Questions
Yes. Each scroll event computes the pixel distance moved since the previous event divided by elapsed time, normalized to a 60fps frame, producing a real "pixels per frame" velocity number. That number (after smoothing) directly sets the blur target in pixels, so a fast flick produces measurably more blur than a slow scroll — verifiable by watching the live meter track both.
Raw scroll events are noisy and irregularly timed, so smoothing the velocity first with an exponential moving average avoids a single jumpy event spiking the number. Then easing the applied blur toward that smoothed velocity target, rather than snapping to it every frame, is what gives the blur a soft ramp instead of a flicker — two lightweight smoothing passes instead of one heavier one.
Scroll events simply stop firing once the page is still, so a separate requestAnimationFrame loop tracks how long it has been since the last scroll event and decays the velocity toward zero if none has arrived within about two frames. Without this check, a scroll that stops instantly (rather than decelerating) would leave the last measured blur amount stuck on screen indefinitely.
The scroll listener is passive and does only cheap arithmetic — no DOM reads that trigger layout. The actual style write (filter) happens once per animation frame in the rAF loop rather than once per scroll event, and filter/blur is GPU-composited, so it stays smooth even during a fast, sustained scroll.
Attach the scroll listener and start the rAF loop in a mount effect, storing velocity and currentBlur in refs rather than plain variables so they persist across renders without triggering re-renders themselves; apply the computed blur directly via a ref's style property. Return a cleanup that removes the scroll listener and cancels the animation frame.