Blur to Clear
Three blocks in a 280 px area: each goes from blur(10px) and opacity 0 to sharp and opaque between entering the frame and reaching its middle — animation-timeline: view(), animation-range: entry cover 50%. No scale, no translation, zero JavaScript: focusing is the only motion.
Updated
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



How it works
Four levels: the frame .demo-preview (centered flex, min-height: 300px, position: relative, #0a0a0f background, white text), the scrolling area .sdc-scroll-container (100% × 280px, overflow-y: auto, scroll-behavior: smooth, 4 px WebKit scrollbar), the stage .sb-blurtoclear-content (a fixed 900 px, padding: 60px 16px, flex column, gap: 30px) and three .sb-blurtoclear-box: padding: 24px, 16 px radius, 135° gradients cyan → violet, violet → pink, green → cyan, a 1.1 rem h4 at weight 800 (Inter set inline, a catalog leftover) and a 0.75 rem p in white at 90%. Measured in Chromium 151 on a blank page: 91 px blocks at 60, 181 and 302 px from the top of the stage, travel 900 − 280 = 620 px. The .sdc-scroll-hint hint “Scrollez ↓” (absolute, 8 px from the bottom of the frame, 10.4 px, white at 30%) pulses from .3 to .7 every 2 s. Empty JavaScript field, no site variable, no console error, same rendering at 390 px and on a white page.
Each block starts at filter: blur(10px); opacity: 0; a single keyframe brings it back, @keyframes sbBlurClear { to { filter: blur(0); opacity: 1 } }. The engine is not time but position: animation-timeline: view() ties progress to the block's journey through the nearest scrolling container — the area, not the page — and animation-range: entry cover 50% bounds the useful window: 0% when the block's top edge crosses the bottom of the area (entry 0%), 100% when its center reaches the center of the area, at 140 px (cover 50%). animation-duration: auto, linear and animation-fill-mode: both do the rest: proportional progress, start state held before the range, end state held after it. For 91 px in 280, the range is 185 px (half block + half area), with opacity = p and blur = 10 × (1 − p): measured with the wheel on the third block, opacity .42 and blur(5.8px) at 100 px of scroll, .69 and 3.1 px at 150, sharp at 207.
The shipped geometry does not treat the three blocks alike. The first (center at 105 px) has already passed the middle at rest: fill-mode: both holds it sharp before any scrolling. The second is caught mid-range — measured at rest opacity .53, blur(4.7px), sharp from 86 px. Only the third plays the whole sequence, from 22 to 207 px. Then nothing over the remaining 413 px and, from 393 px on, the area shows nothing but its background — 227 px of empty travel, due to the stage's fixed 900 px. No transform: a block only moves because the area scrolls, which sets it apart from Blur Reveal (fx-0543, scale(1.1) → 1) and Zoom Into Content (fx-0570, scale(.3) → 1). The animated filter puts each block on its own compositing layer: three blocks cost nothing, fifty are worth measuring.
Accessibility
- prefers-reduced-motion not in the code: measured under emulation, nothing changes — third block at opacity 0 at rest, .42 at 100 px, pulsing hint. The animation is driven by the reader, with no displacement or zoom: a mild case under WCAG 2.3.3, but a block that stays invisible until you scroll is still a nuisance. Add
@media (prefers-reduced-motion: reduce) { .sb-blurtoclear-box { animation: none; filter: none; opacity: 1 } .sdc-scroll-hint { animation: none } }: everything is flat and sharp, as in Firefox. - Keyboard: measured, Tab focuses the area without a
tabindexin Chromium (outline: autoring) as in Firefox; Arrow Down moves 40 px in Chromium (third block at .10), 51 px in Firefox; PageDown 245 px (block sharp), End goes to 620. Safari does not focus a scrolling box withouttabindex="0": add it, withrole="region", anaria-labeland a visible:focus-visible. - Contrast (17.6 px title at weight 800, below the 18.66 px “large text” threshold: 4.5:1 required): white on
#06b6d42.43:1,#8b5cf64.23:1,#ec48993.53:1,#10b9812.54:1 — no gradient end passes; the 12 px paragraph at 90% ranges from 2.22 to 3.75:1. Mid-range, text and block both at 50% over#0a0a0fdrop to 1.9-2.5:1, a transient acceptable if the end state is readable: darken the gradients (#0e7490 → #6d28d9gives 5.4 and 7.1:1). The hint at 30% reads 2.62:1: decorative,aria-hidden="true". - Screen readers and fallback: three
divs with anh4and ap, read whatever the opacity —opacity: 0removes nothing from the accessibility tree, unlikevisibility: hidden. Anh4with no h2/h3 above breaks the page outline. “Scrollez ↓” is hard-coded in French. Whereanimation-timelinedoes not exist (Firefox, Safari before 26), the blocks are sharp from the start: the content never depends on the effect.
Browser compatibility
CSS only, no JavaScript, no dependency: animation-timeline: view(), animation-range with entry and cover, animation-duration: auto, animated filter: blur(). Chrome and Edge 115+, Safari and iOS 26+. Firefox: no version — measured in Firefox 153, CSS.supports('animation-timeline: view()') returns false, animation-duration: auto is rejected and the duration falls back to 0 s: the animation jumps to its to keyframe, the three blocks are sharp and opaque from load (opacity 1, blur(0) over the whole travel), the area scrolls, nothing is hidden or overlapping. No @supports in the code, and none needed: the end state is the readable state. A buying fact: on Firefox, your visitor sees a list of cards with no focusing.
Without animation-timeline (Firefox, Safari and iOS before 26, Chrome and Edge before 115), the declaration is dropped, animation-duration: auto too, and the 0 s animation ends at once: fill-mode: both applies the final keyframe, sharp and opaque blocks, a scrolling area. No script to enable.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="demo-preview">
<div class="sdc-scroll-container">
<div class="sb-blurtoclear-content">
<div class="sb-blurtoclear-box">
<h4>…</h4>
<p>…</p>
</div>
<div class="sb-blurtoclear-box">…</div>
<div class="sb-blurtoclear-box">…</div>
</div>
</div>
<span class="sdc-scroll-hint">…</span>
</div>
The full HTML + CSS + JS of Blur to Clear, ready to paste — plus all 811 effects. Or €299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Reveal range (CSS — animation-range: entry cover 50%) |
entry 0% → cover 50% (185 px for a 91 px block in 280) | entry 0% entry 100%: sharp as soon as the block is fully inside (91 px of travel). cover 20% cover 80%: focusing spread over most of the crossing. entry 25% cover 50%: block invisible for its first 23 pixels. |
Starting blur (CSS — filter: blur(10px)) |
10 px | 20 px (fx-0543's value) erases the shape of the block, 4 px gives a veil. The blur covers text and background: 12 px text is unreadable from 3 px. |
Starting opacity (CSS — opacity: 0) |
0 | At 0, a block outside its range is absent — hence the third one invisible at rest. .35 keeps a blurred silhouette that announces what follows; the keyframe still leads to 1. |
Curve (CSS — animation-timing-function: linear) |
linear | Proportional to the scroll (measured: opacity .42 at 42% of the range). ease-out makes it almost sharp within the first third, ease-in delays sharpness until the center. |
Area height and stage length (CSS — .sdc-scroll-container { height: 280px }, .sb-blurtoclear-content { height: 900px }) |
280 px / 900 px (620 px travel, 227 of them empty) | A block's range is half block + half area: a 400 px area brings each focus pull to 245 px. Set the stage to height: auto so the travel ends at the last block, plus a padding-bottom of half an area if it must reach the center — the setting of the three scenes. |
Blocks (CSS — .sb-blurtoclear-box, :nth-child(2), :nth-child(3)) |
24 px padding, 16 px radius, three 135° gradients | A fourth block reuses the base rule's cyan → violet: add its :nth-child(4) line. Every child is blurred with the block, images included. |
Timeline container (HTML/CSS — .sdc-scroll-container) |
280 px area with overflow-y: auto |
view() picks the nearest scrolling ancestor. Without the area (or with overflow: visible; height: auto), the page drives and each block becomes sharp in the middle of the window. view(block 15%) tightens the range by 15% at the top and bottom. |
“Scrollez ↓” hint (HTML/CSS — .sdc-scroll-hint) |
8 px from the bottom of the frame, 10.4 px, white at 30%, 2 s pulse | Hard-coded French, positioned relative to .demo-preview: without that frame it drops to the bottom of the next positioned ancestor. Delete the span and sdcHintPulse with it. |
FAQ
entry cover 50% range is entirely past at load and fill-mode: both holds the final keyframe. The second runs from −99 to 86 px: at rest it is at 53%, measured opacity .53 and blur(4.7px). Only the third (22 → 207 px) starts from zero. To make the first one play too, push it below the bottom of the area (padding-top: 280px on the stage) or shorten the range (entry 0% entry 100%, which ends before the center). In the three scenes on this page, the first block is deliberately sharp from the start: it shows the arrival state.animation-timeline: view(), zero JavaScript), two settings that change everything. fx-0543 animates three properties — opacity: 0 → 1, blur(20px) → 0, scale(1.1) → 1 — over entry entry 60%: sharp once 60% of its height has entered, 55 px of travel for 91 px, before it is even fully visible, and it shrinks as it sharpens. fx-0551 animates only the opacity and a 10 px blur, with no transform, over entry cover 50%: 185 px, 3.4 times longer, sharpness in the middle of the area. Pick fx-0551 when the content must not deform (product photo, poster, text) or when sharpness must coincide with the reading zone; fx-0543 for a short, theatrical entrance. Zoom Into Content (fx-0570) is the third setting of the same technique: scale(.3) → 1 and blur(8px) over the same range, for a hero block. On Firefox, all three behave the same: content sharp from the start.view() attaches to the nearest scrolling container: without .sdc-scroll-container, the window drives and cover 50% becomes “block center at the center of the screen” — on an 800 px tall phone, the range of a 91 px block is 445 px. The number of blocks is free: the rule is a class, each block carries its own timeline, only the backgrounds are in :nth-child. Two precautions: the last block only reaches the center if the page can still scroll half a window below it (footer or padding-bottom, otherwise prefer entry 0% entry 100%); and every blurred block lives on its own layer, light for a dozen cards, worth measuring beyond. view() reads the vertical axis: nothing reacts to horizontal scrolling.