Immersive Depth Scroll
Three nested translucent frames (150, 100 and 60 px wide), set at three depths, that the pointer shifts, hardest for the farthest ones: the center hollows out like a tunnel and settles back when the pointer leaves — a finger or the arrow keys drive it too; despite the name, page scrolling plays no part, and the frames hold still once the "reduce motion" setting is on.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. 3D has 50 effects, including 8 free. Explore the category →
3 usage examples



How it works
Inside .fx-0049-root, a near-black backdrop, sits .xr-depth-scroll: full width, 300 px tall, with perspective: 400px that listens to the pointer. It stacks three .xr-depth-layer set to position: absolute; inset: 0, each centering a .xr-depth-shape frame with 12 px corners and a 20% indigo border: 150 × 100 px almost transparent, 100 × 70 px a little denser, 60 × 40 px the strongest. The second layer is pushed back 30 px and the third 60 px (translateZ): the perspective shrinks them to 93% and 87%, which hollows out the nesting even at rest. The emoji of the two large frames sit in their top left corner and the small one's in the center, so all three stay readable.
One instance of the script runs for each area on the page. It locates the pointer relative to the center, from −0.5 to +0.5, and shifts each layer by that value times 15, 30 and then 45 px, keeping its depth: the back frame moves three times as much as the front one, so the center seems to open toward the pointer, like a tunnel seen at an angle. With the pointer 5% from the top left corner, the small frame moves 20.25 px on each axis, in Chromium 151 as in Firefox 153. The slide is cushioned over 0.3 s; when the pointer leaves, the three frames return to the center.
Despite the word "Scroll" in the name, page scrolling plays no part: only the pointer drives the frames. Pen and finger go through the same pointer events as the mouse; with touch-action: pan-y, a vertical gesture still scrolls the page and only a sideways swipe hollows the tunnel. On the keyboard side, the area takes focus (tabindex="0") and carries a name (role="group", aria-label); the arrows push the tunnel a tenth per press, and Escape or losing focus recenters it. The layers are direct children of the element holding the perspective, without preserve-3d: their depth does not depend on any intermediate parent.
Accessibility
- Reduced motion: when
prefers-reduced-motion: reduceis on, the@mediarules written under.fx-0049-rootdrop the transition and nail each layer to its rest position (0, −30 and −60 px of depth). The three frames stay nested and readable, with no shift under the pointer. Under the mouse, Chromium, Firefox and WebKit no longer move a single pixel (20 px of shift outside that setting). - Keyboard: the area joins the tab order; its focus ring (
#a5b4fc, 2 px, 9.9:1 on the background) is drawn inward since it spans the full width. With → ×4 then ↑ ×2, the small frame slides 18 px right and 9 px up; Escape or one more Tab brings it back to the center. - Screen readers: you first hear the group's name, "Nested frames: the arrow keys shift them" (
aria-label), then the three emoji. If the frames carry meaning (scales, steps), write it as text, like the museum's scales, and translate thearia-label; if they are decorative, remove thetabindexand hide the area witharia-hidden="true". - Contrast: the frames are deliberately faint (20% indigo border, 5 to 20% fill): they are decorative. To carry text, strengthen the border and the fill as in the museum scene.
Browser compatibility
CSS (perspective on the area, translateZ on its direct children, touch-action, :focus-visible) + JS with pointer events, no dependencies. Measured in Chromium 151, Firefox 153 and WebKit 26.5: same shifts, back frames shrunk by the perspective, two independent areas on one page, still frames under reduced motion; finger shift measured in Chromium in touch mode. No nested 3D here: Playwright's WebKit renders the depths like Chromium, but a real Safari was not checked.
Without 3D transforms, the three frames keep their original size and shift flat: the nesting stays readable. Without :focus-visible, the browser's default focus ring shows instead.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0049-root">
<div class="xr-depth-scroll" tabindex="0" role="group" aria-label="…" style="width:100%;min-height:300px;">
<div class="xr-depth-layer">
<div class="xr-depth-shape">🌎</div>
</div>
… (3 layers: 🌎, ⭐, 💎)
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Shift step (JS — (i + 1) * 15) |
15, 30 and 45, i.e. 7.5, 15 and 22.5 px with the pointer at the edge of the area | For frames of 200 px and more, go up to 30 or 40 (the scenes use 40); at 15, large frames barely move. |
Layer depth (JS — translateZ(-i * 30); CSS — translateZ(-30px) and (-60px) of layers 2 and 3, twice) |
0, −30 and −60 px | The farther back the layers, the smaller they get. Change the value in all three places: script, rest position and reduced motion block. |
Perspective (CSS — perspective: 400px on .xr-depth-scroll) |
400px | At 250px the back shrinks clearly (deeper tunnel); at 800px the three frames almost keep their size. |
Tunnel direction (JS — (i + 1)) |
the back frame moves the most | Replace it with (3 - i): the front frame moves the most, like a classic parallax, and the nesting looks raised instead of hollow. |
Frames (CSS — .xr-depth-layer:nth-child(1) to (3) .xr-depth-shape) |
150 × 100, 100 × 70, 60 × 40 px; indigo at 5, 10 and 20% | Sizes, colors and shapes are free (egg-shaped border-radius for dolls, square corners for a frame). |
Frame content (CSS — rule of the two large frames, align-items and justify-content) |
emoji top left for the two large frames, centered for the small one | Centering all three stacks their content on top of each other. For captions at the bottom, switch to align-items: flex-end; justify-content: center. |
Area height (HTML — style="min-height:300px" on .xr-depth-scroll) |
300 px | The whole area listens to the pointer: a taller area gives a wider gesture for the same shift. |
FAQ
my from the area's position in the window on scroll, instead of the pointer: that is not what this code does.(i + 1) with (3 - i) in the script.