ScrollFree

SVG Blinds Mask Transition

Eight 32.5 px CSS slats stacked in a flex column, pinching one after another toward their midline (scaleY 1 → 0) across the 624 px of an internal scroll zone — only one slat moves at a time, and the caption only appears over the last quarter. No SVG despite the name: ten CSS rules and 17 lines of JavaScript that write --p.

ScrollMaskReveal
Scrollez ici ↓
Hover or click the scene to interact

This effect is free — the Scroll category contains 70 effects total, including 12 free. Effect.Labs has 811 vanilla effects. Explore the category →

Usage examples

Scroll to reveal ↓
Scroll to reveal the laureate

2026 laureate of the Architecture Prize: Atelier Lumen, for the Sées media library.

① The laureate's name behind eight slats — page of an award ceremony

WhenCeremony page: first an opaque screen of eight slats; the visitor scrolls and the slats open one by one onto the name of the year's laureate.
WhyA curtain opening in bands, in a fixed order, top to bottom, is the metaphor of an official unveiling — and the caption, invisible until three quarters of the travel, keeps the name for the end. Corner Morph Overlay (fx-0779) reveals as a circle from a corner: an expansion, not a curtain rising.
SettingsMidnight-blue gradient slats with a 1 px gold hairline, scene stretched to 100% (max-width and border removed), 40 px serif caption without letter-spacing; the “Architecture Prize 2026” eyebrow sits in the top of the photo, the “for the media library” line follows (var(--p) − .85) × 7; the name is repeated in a visually hidden text outside the aria-hidden block; automatic teaser up to --p = 0.5 when entering the viewport.
Maison Delacroix Properties · Valuation · The agency Preview
Scroll to open the blinds ↓
Scroll to open the blinds on the property

Preview: Villa Les Cyprès in Vence, 245 m², 5 bedrooms, pool, sea view, 1,250,000 €.

② A preview property behind a blind — real-estate agency site

WhenMembers-only property page: the facade is masked by an ivory blind; the scrolling visitor opens the slats and discovers the villa, then the price card at the bottom.
WhyThe blind is that of an agency window: the ivory gradient slats look like real blades, and the top-to-bottom opening delivers the sky, the facade, the pool, then the price — in the order of a visit. Corner Morph Cards (fx-0780) rearranges existing cards; it is not a mask.
SettingsSlats linear-gradient(#faf7f0, #ebe5d8 45%, #b9ad98) (light blade, shaded foot), scene in flex: 1 under a 38 px agency bar, caption removed (the price card is a block of the photo, revealed by the last two slats), brown hint on ivory; teaser up to 0.5.
Scroll to dim the lights ↓
Scroll to switch from lunch to dinner

Le Comptoir des Saisons — lunch 12 pm to 2 pm in a bright room, candlelit dinner 7:30 pm to 11 pm, seven-course tasting menu.

③ From lunch to dinner, slat by slat — page of a fine-dining restaurant

WhenRestaurant page: the bright lunch room fills the screen; on scroll, each slat of the bright room closes and lets the candlelit evening room show through.
WhyHere the slats do not hide a flat background: they carry the first image. With background-size: 100% 800% and a position keyed to --k, the eight slats compose the lunch room; as each one pinches, it uncovers the same room at night. Concentric Rings Morph (fx-0781) draws abstract SVG rings — no change of mood.
SettingsSlats: lunch image sliced by background-position: 0 calc(var(--k) × 100% / 7); evening photo in warm gradients with two CSS candles; “Lunch” label above the slats, fading like the hint (1 − var(--p) × 3); “Candlelit dinner” caption bottom left, hours at (var(--p) − .85) × 7; restaurant name in a fixed pill above everything.

How it works

Three layers inside .sbm-scene (100% wide, max-width: 430px, 260px tall, overflow: hidden, --p: 0). On top, .sbm-scroll: a transparent layer in position: absolute; inset: 0, overflow-y: auto, z-index: 3, holding a .sbm-track of height: 340% — 884 px of content for a 260 px window, i.e. 624 px of travel as measured. Underneath, .sbm-stage (z-index: 1, pointer-events: none, aria-hidden) stacks .sbm-photo (blue → violet → pink gradient, centered caption) and .sbm-slats, a flex column of eight span.sbm-slat in flex: 1 1 0% — 32.5 px each — whose rgb(13, 13, 22) background is the scene's own: closed, the whole thing is a flat panel. The JavaScript (17 lines, ES5) listens to scroll passively on #sbmScroll, batches through requestAnimationFrame and writes --p = scrollTop / (scrollHeight − clientHeight), rounded to four decimals, on the scene. No SVG anywhere: the name comes from the catalog, the mask is made of stacked spans.

Each slat carries its rank inline (--k: 0 to 7) and a single rule does everything: transform: scaleY(clamp(0, 1 − (var(--p) × 9 − var(--k)), 1)), transform-origin: 50% 50%. Slat k stays at 1 while --p < k/9 and reaches 0 at (k + 1)/9: 69 px of scrolling per slat, strictly one after the other — measured at --p = 0.5, slats 0 to 3 are at 0, slat 4 at 0.500 (16.3 px tall out of 32.5), slats 5 to 7 at 1. The centered origin means the slat pinches toward its midline: the photo shows through both edges of the band, not from the bottom like a rising blind. The multiplier 9 for eight slats reserves the last ninth (--p from 0.889 to 1, 69 px) where nothing moves except the caption: opacity: clamp(0, (var(--p) − .75) × 4, 1), invisible until 468 px of scrolling, full at 624. The “Scrollez ici ↓” hint follows calc(1 − var(--p) × 3): gone at a third of the travel (208 px).

Measured in Chromium on a blank page: no console error, no seam between slats at --p = 0 (a uniform pixel column). The scroll layer covers everything (elementFromPoint returns .sbm-track): the wheel is captured over 624 px, then the page resumes — chaining measured, the page had not moved at mid-travel and advanced 200 px after the end stop. From the keyboard, the zone (tabindex="0", role="region") responds: arrow down = 40 px (--p + 0.064), space = 227 px. Two limits: --p is only recomputed on the next scroll — after going from 260 to 400 px tall with scrollTop = 312, it stays at 0.5000 while the true ratio is 0.325 (no resize listener); and scrollbar-width: thin leaves a thin bar on the right edge of the photo wherever scrollbars are classic (Windows, Linux — 0 px measured on macOS). The inline style="--p: 0.0000;" in the sold HTML is a trace of the extraction, identical to the initial state. 1.6 KB of CSS, 0.56 KB of JS.

Accessibility

  • prefers-reduced-motion not handled — measured under emulation: same transforms at --p = 0.5. The effect never moves on its own (everything follows the wheel), but eight successive pinches on every notch are still motion. Under @media (prefers-reduced-motion: reduce), replace the geometry with a fade driven by the same variable: .sbm-slat { transform: none } and .sbm-slats { opacity: calc(1 − var(--p) × 1.2) } — the content is revealed at the same pace, without slicing.
  • The revealed content sits in an aria-hidden="true" block (.sbm-stage) and under the scroll layer (z-index: 3): the caption — a laureate's name, an address — is neither read by a screen reader, nor selectable, nor clickable. Write the meaningful text a second time outside the scene, visible or visually hidden. And without JavaScript, --p stays at 0: the panel never opens.
  • Keyboard: the zone is focusable (tabindex="0", role="region", aria-label hard-coded in French), with a 2 px #a78bfa outline on :focus-visible (7.1:1 on the background); arrow down = 40 px, space = 227 px measured. Contrast: white 20.8 px bold caption on the gradient = 11.7:1 (blue), 5.7:1 (violet), 4.6:1 (pink) — compliant; hint rgb(143, 143, 176) = 6.2:1 but at 0.6rem, i.e. 9.6 px, and in French with no data-en: raise it to 0.75 rem and translate it.
  • Scroll trap: the transparent layer absorbs the wheel and the finger over 340% of the zone's height (624 px here) before handing back to the page — measured, the page does not move until the zone hits its end stop. Keep the scene short (260 px as sold), never make it full-screen, and leave the hint visible: it is the only signal that the zone scrolls from the inside.

Browser compatibility

Requires clamp() and CSS variables inside calc(), inset, transform, Element.closest() and requestAnimationFrame; ES5 JavaScript, zero dependencies. Everything is classic CSS driven by a scroll listener: unlike the category's 45 “native CSS” effects (Scroll-Driven Fade, View Timeline Scale, Clip Path Reveal…) built on animation-timeline, which no version of Firefox animates, this one works in Firefox.

Chrome 87+✓ Full
Firefox 75+✓ Full (focus outline from 85)
Safari 14.1+✓ Full (focus outline from 15.4)
Edge 87+✓ Full
Mobile iOS 14.5+✓ Full (finger scrolling)
Android Chrome✓ Full (finger scrolling)

Without JavaScript, --p stays at 0: closed panel, hint displayed, the zone scrolls for nothing and nothing is revealed — plan a class set by the script (or a noscript block) that opens the slats. Without clamp() (browsers older than 2020), the transform declaration is ignored: slats frozen, closed. Without :focus-visible, the rule is dropped and the browser draws its default outline.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="sbm-scene" style="--p: 0.0000;">
  <div class="sbm-scroll" id="sbmScroll" tabindex="0" role="region" aria-label="Zone de démonstration au défilement">
    <div class="sbm-track" style="height:340%">
    </div>
  </div>
  <div class="sbm-stage" aria-hidden="true">
    <div class="sbm-photo">
      <div class="sbm-caption">RÉVÉLÉ</div>
    </div>
    <div class="sbm-slats">
      <span class="sbm-slat" style="--k:0">
      </span>
      <span class="sbm-slat" style="--k:1">
      </span>
      <span class="sbm-slat" style="--k:2">
      </span>
      <span class="sbm-slat" style="--k:3">
      </span>
      <span class="sbm-slat" style="--k:4">
      </span>
      <span class="sbm-slat" style="--k:5">
      </span>
      <span class="sbm-slat" style="--k:6">
      </span>
      <span class="sbm-slat" style="--k:7">
      </span>
    </div>
  </div>
  <span class="sbm-hint">Scrollez ici ↓</span>
</div>
CSS
.sbm-scene  {
   position: relative;
   width: 100%;
   max-width: 430px;
   height: 260px;
   border-radius: 12px;
   overflow: hidden;
   border: 1px solid rgba(255, 255, 255, 0.08);
   background: rgb(13, 13, 22);
   --p: 0;
   }

.sbm-scroll  {
   position: absolute;
   inset: 0px;
   overflow-y: auto;
   z-index: 3;
   scrollbar-width: thin;
   }

.sbm-stage  {
   position: absolute;
   inset: 0px;
   z-index: 1;
   pointer-events: none;
   }

.sbm-hint  {
   position: absolute;
   right: 10px;
   bottom: 8px;
   z-index: 2;
   font-size: 0.6rem;
   letter-spacing: 0.12em;
   text-transform: uppercase;
   color: rgb(143, 143, 176);
   opacity: calc(1 - var(--p) * 3);
   pointer-events: none;
   }

.sbm-scroll:focus-visible  {
   outline: rgb(167, 139, 250) solid 2px;
   outline-offset: -2px;
   }

.sbm-track  {
   width: 100%;
   }

.sbm-photo  {
   position: absolute;
   inset: 0px;
   display: grid;
   place-items: center;
   background: radial-gradient(circle at 30% 25%, rgba(34, 255, 170, 0.25), transparent 55%), linear-gradient(160deg, rgb(20, 58, 94), rgb(124, 58, 237) 70%, rgb(219, 39, 119));
   }

.sbm-caption  {
   font-size: 1.3rem;
   font-weight: 800;
   letter-spacing: 0.3em;
   color: rgb(255, 255, 255);
   text-shadow: rgba(0, 0, 0, 0.6) 0px 2px 12px;
   opacity: clamp(0, (var(--p) - .75) * 4, 1);
   }

.sbm-slats  {
   position: absolute;
   inset: 0px;
   display: flex;
   flex-direction: column;
   }

.sbm-slat  {
   flex: 1 1 0%;
   background: rgb(13, 13, 22);
   transform: scaleY(clamp(0, 1 - (var(--p) * 9 - var(--k)), 1));
   transform-origin: 50% 50%;
   }
JavaScript (fx-0778)
(function() {
  /* fx-0778 — SVG Blinds Mask : progression de scroll → variable CSS --p (rAF, passif) */
  var sc = document.getElementById('sbmScroll');
  if (!sc) return;
  var host = sc.closest('.sbm-scene');
  var raf = null;
  function update() {
    raf = null;
    var max = sc.scrollHeight - sc.clientHeight;
    var p = max > 0 ? sc.scrollTop / max : 0;
    host.style.setProperty('--p', p.toFixed(4));
  }
  sc.addEventListener('scroll', function() {
    if (!raf) raf = requestAnimationFrame(update);
  }, { passive: true });
  update();
})();

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Number of slats (HTML — span.sbm-slat with --k) and multiplier (CSS — var(--p) * 9) 8 slats, × 9 The multiplier must be n + 1 to keep one slat at a time and a final ninth for the caption; at plain n, the last slat ends at --p = 1, together with the caption. Twelve 21.7 px slats with × 13: 48 px of scrolling each.
Travel (HTML — .sbm-track height: 340%) 340% = 624 px for 260 px tall Wheel distance between closed and open. 200%: 260 px, 29 px per slat, abrupt; 500%: 1,040 px, slow. The page does not scroll until this travel is done.
Overlap (CSS — var(--p) * 9 − var(--k)) none: one slat at a time Write var(--p) * 5 − var(--k) * .5: each slat takes two tenths of the travel and the next one starts halfway — two slats in motion, the “staggered” effect the catalog promises. General form --p × m − --k × d: duration 1/m, offset d/m.
Pinch direction (CSS — transform-origin: 50% 50%) center: the band opens from both edges 50% 0: the slat folds against its top edge, the photo rises from the bottom of each band — the blind that lifts; 50% 100%: the opposite. Same cost, no other line to change.
Caption entry (CSS — (var(--p) − .75) * 4) from 0.75 to 1 (468 → 624 px) The first term sets the start, the second the speed (a quarter of the travel). (var(--p) − .5) * 2: from mid-travel, over the whole second half. Below 0.75, the caption shows up behind slats that are still closed.
Slat material (CSS — .sbm-slat background) rgb(13, 13, 22), the scene background A vertical gradient per slat makes the eight bands visible at rest (blind blade). To reveal one image through another: background-size: 100% 800% (eight times the height) and background-position: 0 calc(var(--k) * 100% / 7), each slat shows its slice.
Hint (CSS — .sbm-hint, 0.6rem, calc(1 − var(--p) * 3)) 9.6 px, gone at --p = 0.333 Text hard-coded in French in the HTML. Raise it to 0.75rem (12 px); * 6 fades it at a sixth of the travel, * 1 keeps it to the end.
Recomputing --p (JS — update()) on scroll only Add window.addEventListener('resize', update): after a height change, the value stays wrong until the next notch (0.5000 for a true 0.325 as measured). The toFixed(4) is enough: 624 px give 0.0016 per pixel.

FAQ

The catalog talks about “staggered” slats: why does only one move at a time?

Because the offset between slats (1, through − var(--k)) equals the duration of each pinch (1, the width of the clamp): slat k starts exactly when slat k − 1 ends. Measured at --p = 0.5: four slats at 0, one at 0.5, three at 1 — never two in motion. It is staggered in the strict sense (each starts in turn), but with no overlap, which gives a clean, almost mechanical sweep. For real tiling, bring the offset below the duration: var(--p) * 5 − var(--k) * .5 starts the next slat halfway through the previous one.

The slats do not rise, they flatten in place — how do I get a blind that lifts?

That is transform-origin: 50% 50%: scaleY shrinks the slat toward its own center, so the photo breaks through the top and the bottom of each band at once — at mid-travel, slat 4 is a 16 px line between two slivers of photo. Move the origin to 50% 0: the slat folds against its top edge and the photo rises from the bottom of the band, like a blind blade lifting. 50% 100% gives the opposite (the curtain drops). No JavaScript change, and the cost stays that of a transform: compositor only, no layout.

Blinds Mask or Corner Morph Overlay (fx-0779) to reveal a section?

Both effects share the same skeleton — a transparent layer with overflow-y: auto, --p written by the same 17-line script — and differ by the mask. fx-0779 clips a single layer with clip-path: circle(calc(6% + var(--p) × 140%) at 90% 88%): a disc born bottom right that grows without steps, a continuous reveal from a point. fx-0778 uses no clip-path: eight spans in scaleY, each on its ninth of the travel, a band-by-band reveal from top to bottom at a readable cadence. Pick the circle for a page transition or a mood change with no direction; the slats when order matters (a title at the top, a price at the bottom) or when the metaphor of the blind, the curtain or the shop window serves the message.