Scroll✨ Premium

Clip Path Reveal

Three 91 px blocks in a 280 px box scrolling over 620 px, each revealed by clip-path: inset() to inset(0) on a view() timeline, range entry → cover 60%: the first opens left to right, the second top to bottom, the third bottom to top, with no fade and no movement — the content is already there, the mask is what opens, over 223 px of scrolling per block. CSS only, zero JavaScript.

CSSClip-pathReveal

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

Roller-applied paint rendering — product page of a wall paint — Clip Path Reveal example 1

① Roller-applied paint rendering — product page of a wall paint

WhenA tint's page: name, price per litre and button in a fixed column, then, in the scrolling area, three rendering swatches (matt, satin, velvet) revealed left to right like a roller pass.
Whyinset(0 100% 0 0) → inset(0) wipes horizontally with no fade: the swatch appears at full color behind the edge of the mask, exactly like paint behind the roller. Blur to Clear (fx-0551) would make the tint emerge from a blur, which no paint does; Fade Up (fx-0535) would make it rise and pale.
SettingsFixed 190 px column, absolute scrolling box on the right (350 × 360); stage at height: auto, gap: 10px; three 104 px swatches (tint #b8623a, dark band at the bottom for the text, satin and velvet variants) all forced to inset(0 100% 0 0) — the directions of :nth-child(2) and (3) are overridden; range unchanged, so 278 px of scrolling per swatch (0.6 × (360 + 104)); at rest, 1st open, 2nd at 69%, 3rd at 28%; travel 268 px. The automatic scrolling is a demo device.
Scoreboard of an esports final — the stage screen during the break — Clip Path Reveal example 2

② Scoreboard of an esports final — the stage screen during the break

WhenPresentation screen: title of the final, then four score lines (team, kills, objectives, points) revealed edge to edge as the host scrolls.
WhyA score line must appear whole and sharp, not rise or fade: the clip-path wipe reads like a broadcast “wipe”, and the direction is set line by line in CSS — the third opens from the bottom, as in the sold code, the fourth from the right.
Settings56 px bar, 304 px scrolling box; lines = .sb-clippath-box as a grid, 58 px tall, #12152a background, left rule in the team color; directions: 1st left → right, 2nd top → bottom, 3rd bottom → top (sold code), 4th right → left via inset(0 0 0 100%) added in the scene; 217 px of scrolling per line, travel 232 px; at rest, 1st open, 2nd at 68%, 3rd at 37%, 4th at 6%.
Curtain up on the season's new productions — “Season” page of a municipal theatre — Clip Path Reveal example 3

③ Curtain up on the season's new productions — “Season” page of a municipal theatre

WhenSeason page: theatre name and box office in the bar, then, in the scrolling area, an editorial and three show posters set in front of a red curtain, each revealed from the bottom up as the reader scrolls down, like the curtain rising on a set that is already in place.
Whyinset(100% 0 0) → inset(0) reveals from the bottom without moving anything: the poster is whole behind the mask from the start, only the edge of the curtain rises — exactly what a stage curtain does, the set is in place before the audience sees it. Fade Up (fx-0535) would move the poster itself upward while brightening it, Blur to Clear (fx-0551) would pull it out of a blur: no curtain does either.
Settings52 px bar, 308 px scrolling box; stage turned into a row (flex-direction: row) of three 160 × 200 px frames, curtain background (repeating-linear-gradient at 90°, 26 px folds) and golden rod as a ::before; the poster is the .sb-clippath-box, absolute over the whole frame, inset(100% 0 0) forced on all three; the frame is trimmed with clip-path: inset(0 round 6px), not overflow: hidden — measured: with overflow: hidden, the frame becomes a scroll container, view() attaches to it and the poster stays frozen at inset(16.7% 0 0); 305 px of scrolling per poster (0.6 × (308 + 200)), travel 318 px; at rest, posters sit 165 px below the top of the box and are 47% revealed; all three are whole at 165 px of scrolling. The automatic scrolling is a demo device.

How it works

The sold HTML: the frame .demo-preview (centered flex, min-height: 300px, position: relative, #0a0a0f background, white text), the box .sdc-scroll-container (100% × 280px, overflow-y: auto, scroll-behavior: smooth), the stage .sb-clippath-content (flex column, gap: 30px, padding: 60px 16px, 900 px tall), three .sb-clippath-box (padding: 24px, border-radius: 16px, centered text, 90° gradients with five, three and three stops) with a 1.1 rem weight-800 h4 and a 0.75 rem p in white at 90%, and the hint .sdc-scroll-hint “Scrollez ↓” (absolute, 8 px from the bottom of the frame, white at 30%, pulsing to 70% every 2 s). Measured at 1,280 px wide: 91 px blocks placed 60, 181 and 302 px from the top of the stage, travel 900 − 280 = 620 px. Empty JS field, no var(--…), no console error; the only inline style (the Inter, -apple-system… stack on each h4) loads nothing: Inter absent, system font.

A single @keyframes sbClipReveal { to { clip-path: inset(0) } }, and each block declares it in longhand: animation-name, animation-timeline: view(), animation-range: entry cover 60%, animation-duration: auto, animation-timing-function: linear, animation-fill-mode: both. The starting point is each block's own clip-path: inset(0 100% 0 0) (masked from the right, so it opens left to right), inset(0 0 100%) on :nth-child(2) (masked from the bottom, opens top to bottom), inset(100% 0 0) on :nth-child(3) (masked from the top, opens bottom to top). Nothing else moves: opacity 1 and transform: none read at every position. view() with no argument takes the nearest scroll container, here the 280 px box, on the block axis; the range entry → cover 60% starts when the top of the block touches the bottom of the box and ends at 60% of its full crossing, i.e. 0.6 × (280 + 91) ≈ 223 px of scrolling per block — the mask finishes opening when the top of the block is 57 px from the top of the box. Linear curve: the horizontal wipe covers 1,232 px of width in 223 px of wheel (5.5 px per scrolled pixel), the vertical ones 91 px (0.4 px per pixel). inset(0 100%0 0) is written without a space in the sold code (a minification leftover): the tokenizer splits 100%0 into 100% then 0, measured valid in Chromium, Firefox and WebKit — write 100% 0 if you edit it.

Measured in Chromium, box at rest: the first block, placed at 60 px, has already covered 220 of the 223 px of its range — inset(0 1.17% 0 0), 14 px still masked on the right; the second is 44% open (inset(0 0 55.5%), 40 of 91 px visible); the third, at 302 px, has not entered yet: inset(100% 0 0), invisible. At 60 px of scrolling: 100 / 71 / 17%; at 120: 100 / 98 / 44%; at 180: 100 / 100 / 71%; at 240: 98% for the third; at 300 px, 48% of the travel, all three are at inset(0). Scrolling back closes the masks the other way (the progress follows the scroll, not time). From 393 to 620 px, the last block has left through the top and the box shows only its background: 227 px of empty travel, a consequence of the 900 px fixed on the stage. The frame brings its own dark background, so the effect reads on a white page; no id and no script: two copies coexist on the same page. scroll-behavior: smooth only affects programmatic positions: scrollTop = 300 reads back 1, then lands in 264 ms; the wheel is instant.

Accessibility

  • prefers-reduced-motion not in the code: measured under emulation, same clip-path values at every position and the hint still pulsing. The wipe is driven by the reader (nothing moves without them, except the hint) and displaces no content — only the edge of a mask advances —, which makes it an interaction-triggered animation (WCAG 2.3.3) with no vestibular component. To show everything flat when the preference is on, add @media (prefers-reduced-motion: reduce) { .sb-clippath-box { animation: none; clip-path: inset(0) } .sdc-scroll-hint { animation: none } }.
  • Keyboard: Chromium and Firefox focus the box on Tab without a tabindex (measured: activeElement = .sdc-scroll-container); WebKit does not (focus stayed on body, Arrow Down did nothing). Add tabindex="0" and a visible :focus-visible on .sdc-scroll-container. Once the box has focus: Arrow Down = 40 px (18% of a wipe), Page Down = 285 px (all three blocks open at once), End = 620. With a finger, the gesture must start inside the box; overscroll-behavior: contain keeps the page from being dragged along at the end of the travel.
  • Contrast: the white 17.6 px h4 (below the “large text” threshold) reads 3.7:1 on the rose, 2.1:1 on the amber, 2.3:1 on the green and 2.4:1 on the cyan of the first gradient; only the indigo of the second reaches 4.5:1; the 12 px p in white at 90% falls between 2.0:1 and 3.9:1. No shipped block passes across its full width: darken the gradients or put the text on a dark band, as the three scenes on this page do. The hint at 30% reads 2.6:1 static, 1.2:1 to 1.8:1 under the pulse: the animated opacity (.3 → .7) multiplies with the color's.
  • Screen readers and fallback: clip-path removes nothing from the accessibility tree — the three blocks are read in full, masked or not, and there is nothing to announce. Three h4s with no h2/h3 above break the page outline: use the level that follows your current heading. The “Scrollez ↓” hint is French whatever the page language: put aria-hidden="true" on it or remove it. Where animation-timeline does not exist (Firefox, measured), the three blocks are at inset(0) from load: everything is readable, flat, as long as animation-fill-mode: both stays in place (see “Fill mode”).

Browser compatibility

CSS only, zero JavaScript, zero dependencies. Three recent properties: animation-timeline: view(), animation-range and animation-duration: auto — Chrome and Edge 115, Safari 26 and iOS 26. The rest is old: clip-path: inset(), flexbox, gradients, scroll-behavior, ::-webkit-scrollbar (Blink and WebKit only, Firefox keeps its native bar). Firefox: no version renders scroll-driven animations. Measured on the sold code, blank page: CSS.supports('animation-timeline: view()') false, and the three blocks displayed in full from rest (inset(0)), before any scrolling — the box scrolls, the hint pulses, no mask opens: the Firefox visitor sees three colored cards, flat, complete and readable. No @supports fallback in the code: it does not need one, the final state is the whole content.

Chrome 115+✓ Full
Firefox✓ No version: whole blocks from load, no wipe (measured)
Safari 26+✓ Full (measured in WebKit: same values as Chromium)
Edge 115+✓ Full
Mobile iOS 26+✓ Full (touch scrolling of the box)
Android Chrome 115+✓ Full

Without animation-timeline (Firefox, Chrome and Edge before 115, Safari before 26): animation-duration: auto is ignored, the duration falls back to 0 s, the animation is finished before the first paint and animation-fill-mode: both leaves the last frame, inset(0) — whole blocks, no wipe, a box that scrolls like an ordinary area. Without ::-webkit-scrollbar, the system bar; without scroll-behavior, programmatic scrolls without easing. No script: nothing to lose.

The code

HTML structure to paste into your page (the CSS + JS come with Premium):

index.html — structure
<div class="demo-preview">
  <div class="sdc-scroll-container">
    <div class="sb-clippath-content">
      <div class="sb-clippath-box">
        <h4>…</h4>
        <p>…</p>
      </div>
      <div class="sb-clippath-box">
        <h4>…</h4>
        <p>…</p>
      </div>
      <div class="sb-clippath-box">
        <h4>…</h4>
        <p>…</p>
      </div>
    </div>
  </div>
  <span class="sdc-scroll-hint">…</span>
</div>
Unlock the full code — €9.90/mo excl. tax

The full HTML + CSS + JS of Clip Path Reveal, ready to paste — plus all 811 effects. Or €299 once, lifetime access.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Opening direction (CSS — starting clip-path on .sb-clippath-box, :nth-child(2), :nth-child(3)) inset(0 100% 0 0) / inset(0 0 100%) / inset(100% 0 0) The side at 100% is the one that masks: inset(0 0 0 100%) opens right to left (4th line of the esports scene), inset(0 50%) opens from the center toward both edges, inset(50% 0) from the middle line. A round 16px after the values keeps the corners rounded during the wipe; the end, inset(0), is shared by all three.
Range (CSS — animation-range: entry cover 60%) entry → cover 60% (223 px per block) Wipe length = 60% of the crossing, i.e. 0.6 × (box height + block height). entry alone (= entry 0% entry 100%) ends as soon as the block is just inside: 91 px here, 2.5 times snappier. entry 20% cover 60% delays the start, which keeps a block placed high on the stage from being already open at rest.
Timeline (CSS — animation-timeline: view()) view() — nearest scroll container, block axis view(inline) follows a horizontal scroll; view(block 20% 0) shrinks the visibility area and delays the entry; scroll() ties the three blocks to the progress of the whole box and opens them together. Beware: any ancestor with overflow: hidden becomes the reference for view() and freezes the animation (measured in the theatre scene).
Heights (CSS — .sdc-scroll-container { height: 280px }, .sb-clippath-content { height: 900px; padding: 60px 16px; gap: 30px }) 280 px / 900 px (620 px travel, 227 of them empty) The box height enters the wipe length; the stage height makes the travel. At height: auto, the stage measures 453 px and the travel 173: the last block only opens to 88% — keep a padding-bottom of at least 280 px or some content below the blocks.
Curve (CSS — animation-timing-function: linear) linear With a scroll timeline, the function maps position to progress: ease-out opens fast then slows, ease-in the reverse, steps(8) cuts the wipe into eight notches. The pace stays dictated by the reader's hand.
Fill mode (CSS — animation-fill-mode: both) both Essential on both sides: in a browser that understands the timeline, none would give the block back its starting clip-path once the range is exceeded — it would vanish after opening; in a browser that does not, it is both that holds the final frame inset(0) (measured in Firefox). forwards is enough for both cases.
“Scrollez ↓” hint (CSS — .sdc-scroll-hint, @keyframes sdcHintPulse) 8 px from the bottom of the frame, white at 30%, 2 s pulse Hard-coded French, absolute relative to .demo-preview — without that frame in position: relative, it drops to the bottom of the page. To remove it, delete the span and the @keyframes; to keep it, translate it and set aria-hidden="true".

FAQ

Yes, and it is measured: Firefox renders scroll-driven animations in no version. It ignores animation-timeline: view() and animation-duration: auto, the duration falls back to 0 s and animation-fill-mode: both keeps the last frame, inset(0): the three cards are displayed in full from load, the box scrolls, the hint pulses. Nothing is lost or hidden, only the wipe is missing — hence no @supports fallback. If the wipe is essential everywhere, it has to be driven in JavaScript (an IntersectionObserver or a scroll listener writing clip-path), which this effect does not do; otherwise, accept a flat rendering for 3 to 4% of visitors.
Geometry. The range starts when the top of the block touches the bottom of the box and lasts 223 px; the first block sits at 60 px (the padding-top), so at rest it has already covered 220 of its 223 px — inset(0 1.17% 0 0), 14 px still masked. The third is at 302 px, below the edge of the box: not entered, 100% masked, complete only at 300 px of scrolling, while the travel is 620 px, 227 of them empty. For the first block to wipe under the reader's eyes too, place it below the fold (padding-top: 280px) or delay the start with animation-range: entry 20% cover 60%; to remove the empty travel, switch the stage to height: auto with enough padding-bottom.
Same mask, two engines. fx-0652 animates a single word, “REVEAL” at 2.5 rem weight 800, from inset(0 100% 0 0) to inset(0) with a time-based animation: animation: 1s ease 0s 1 normal forwards clipRevealAnim — it plays once, on load, in one second, whatever the scroll position, and works everywhere, Firefox included. fx-0553 replaces time with position: animation-timeline: view() ties each block to its entry into the box, the progress follows the wheel both ways, and three opening directions coexist (right, top, bottom) — at the cost of Chrome 115 / Safari 26 as the floor and a flat rendering in Firefox. Choose by trigger: a heading that should reveal on arrival on the page, fx-0652; blocks that should reveal when the reader reaches them, fx-0553.