Parallax Layers
A night scene drawn in CSS (stars, moon, border-triangle ridges, mist, ground) pinned with position: sticky inside a 280 px scroller: over a 1,400 px travel, five layers slide on the named timeline --parallaxScroll, from 40 px of amplitude for the stars to 160 px for the mist. Zero JavaScript; Firefox sees the scene flat.
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
The sold HTML nests three frames and five layers. The .demo-preview frame (centered flex, min-height: 300px, position: relative, #0a0a0f background) holds the scroller .parallax-container (100% × 280 px, overflow: hidden auto, scroll-timeline: --parallaxScroll y), which holds the track .parallax-scene (height: 600%, i.e. 1,680 px), which holds the viewport .parallax-viewport (position: sticky; top: 0, 280 px, overflow: hidden). The travel is 1,680 − 280 = 1,400 px and the viewport, stuck to the top of the scroller, never leaves the frame (measured: constant top edge from the first pixel to the last). Inside, five layers in position: absolute: .parallax-stars (100% of the viewport, eight white 2 px .p-star dots — two of 3 px — between 5 and 30% from the top, opacities .3 to .8), .parallax-moon (30 px, top: 20px; right: 30px, radial gradient #e0e7ff → #a5b4fc, 20 px halo), then three bands anchored at the bottom by .slr-parallax-layer (bottom: 0): .slr-parallax-bg (180 px of indigo gradient, whose two ridges are border triangles of 280 × 140 px at left: -10% and 240 × 110 px at right: -5%), .parallax-mid (120 px, whose 80 px ::before carries two violet and indigo radial gradients: the mist) and .parallax-fg (60 px, rgba(15,15,26,.95) to transparent: the ground). Below the scroller, the hint .slr-scroll-hint “Scrollez pour le parallax” (11.2 px, white at 40%, bottom: 8px of the frame) bounces 4 px every 2 s. The JavaScript field is empty.
The engine is two properties. scroll-timeline: --parallaxScroll y on the scroller declares a named timeline that follows its vertical position; each layer attaches its animation to it with animation-timeline: --parallaxScroll, plus animation-duration: auto, animation-timing-function: linear, animation-fill-mode: both and animation-range: normal: progress equals scrollTop ÷ 1400. Four @keyframes, all translateY: parallaxStars 20 → −20 px (stars and moon), parallaxSlow 40 → −40 px (ridges), parallaxMed 80 → −80 px (mist), parallaxFast 120 → −30 px (ground). The closer the layer, the farther it runs: 40, 80, 160 and 150 px over the same travel, i.e. 1 px per 35 px scrolled at the back and 1 px per 8.75 for the mist. Measured in Chromium 151 and WebKit 26.5, blank page, identical values: at 140 px of scrolling (10%), 16 / 32 / 64 / 105 px; at 700 px (50%), 0 / 0 / 0 / 45; at 1,400 px, −20 / −40 / −80 / −30. Two copies pasted in the same page give two scrollers and two independent timelines (measured: each pushed to 700 px, each at 0) — no id, nothing to rename.
At rest, the scene is sunk: the ridge bases are 40 px below the bottom edge, the mist and the ground are entirely out of frame, the moon is 40 px from the top. As you scroll, everything rises at different speeds; halfway the ridges touch the edge and the ground shows its first 15 pixels; at the end of the travel the ridges float 40 px above the edge, the ground occupies the 208–268 px band of the viewport and leaves 30 px of frame background under it (its last keyframe is −30, not 0), the moon touches the top edge. The viewport is transparent: the sky is the background of .demo-preview. Two rules hold it all together. The scroller's fixed height: with height: 100% under a parent with no definite height, it collapses to the height of its content, the travel is 0 and nothing moves (measured in all three engines). And the viewport's overflow: hidden, which clips the overflowing ridges and the ground sunk 120 px at rest — without it, the viewport would grow its own scrollbar.
Accessibility
- prefers-reduced-motion not in the code: measured under emulation, the layers still follow the scroll (0 / 0 / 0 / 45 px halfway, as without the preference) and the hint keeps bouncing. Nothing is hidden, though: the parallax only advances with the reader's gesture, only the bounce loops on its own. Add
@media (prefers-reduced-motion: reduce) { .slr-scroll-hint { animation: none } }; to freeze the layers too, reuse the selector list of the@supportsfallback inside that media query: the scene stays flat and complete. - Keyboard: measured, Tab focuses the scroller without a
tabindexin Chromium 151 and Firefox 153 (activeElement=.parallax-container): Arrow Down moves 40 px (51 in Firefox), Page Down 285 (303), End goes all the way, 1,400. WebKit 26.5 does not focus the area: focus stays onbody, the arrows do nothing. Addtabindex="0", anaria-labeland a:focus-visiblestyle. With a finger, the gesture must start inside the area;overscroll-behavior: containkeeps the page from being dragged along at the end of the travel. - Contrast: the only text is the hint, 11.2 px in white at 40% on
#0a0a0f— renderedrgb(108,108,111), 3.8:1, below the 4.5:1 threshold for small text; it is hard-coded in French. Raise it to 60% (rgba(255,255,255,.6)), translate it or remove thespan— the@keyframes hintBouncegoes with it. Any text placed on the scene must hold its contrast on the sky and on the lighter ridges. - Screen readers and fallback: fifteen empty
divs andspans plus one French sentence — nothing to read. Putaria-hidden="true"on.parallax-viewport(orrole="img"and anaria-labelon the scroller if the scene illustrates something) and keep the useful content outside the scene: a screen reader does not scroll to reveal a ground band. Without scroll-driven animation support, the@supportsfallback shows the complete, still scene; without any CSS, only the hint is left.
Browser compatibility
CSS only: scroll-timeline (named timeline), animation-timeline, animation-range, animation-duration: auto, position: sticky, overflow: hidden auto, gradients, border triangles, five @keyframes. Zero dependencies, zero JavaScript. Scroll-driven animations exist in Chrome and Edge 115+, Safari and iOS 26+ (measured in WebKit 26.5: same values as Chromium 151 at every position). Firefox: no version. Measured in Firefox 153: CSS.supports('animation-timeline: scroll()') returns false and animation-duration: auto computes to 0s. The sold code's @supports not (animation-timeline: scroll()) fallback sets animation: none on the five layers: each stays at its rest position (transform: none measured at 0, 350, 700, 1,050 and 1,400 px of scrolling) — stars and moon at the top, ridges and mist sitting on the bottom edge, ground flush with the edge. The scroller scrolls normally over 1,400 px, the viewport stays pinned, the hint bounces: a Firefox visitor sees a still, complete night scene that scrolling does not move. Without that fallback, they would see the last keyframes: layers raised 20 to 80 px and a 30 px empty strip under the ground (measured).
The fallback hides nothing: it cancels the five animations and the scene shows flat and complete — the mid-travel composition, ground included. If the motion is the message, hide the invitation in the same block: .slr-scroll-hint { display: none }. For a real parallax everywhere, you need a scroll listener writing the transforms, as Parallax Layers (fx-0576) does.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="demo-preview">
<div class="parallax-container">
<div class="parallax-scene">
<div class="parallax-viewport">
<div class="parallax-stars slr-parallax-layer">
<span class="p-star"></span><span class="p-star"></span><span class="p-star"></span><span class="p-star"></span>
<span class="p-star"></span><span class="p-star"></span><span class="p-star"></span><span class="p-star"></span>
</div>
<div class="parallax-moon"></div>
<div class="slr-parallax-bg slr-parallax-layer"></div>
<div class="parallax-mid slr-parallax-layer"></div>
<div class="parallax-fg slr-parallax-layer"></div>
</div>
</div>
</div>
<span class="slr-scroll-hint">…</span>
</div>
The full HTML + CSS + JS of Parallax Layers, 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 |
|---|---|---|
Layer amplitudes (CSS — @keyframes parallaxStars / parallaxSlow / parallaxMed / parallaxFast) |
20 → −20, 40 → −40, 80 → −80, 120 → −30 px | Fixed pixels whatever the travel: over 1,400 px, 40 px is 1 px per 35 scrolled; over 400 px, 1 per 10. Keep the closest layer the fastest, that is what makes the depth. End parallaxFast at translateY(0) so the ground stays flush with the edge at the end of the travel. |
Travel (CSS — .parallax-scene { height: 600% }) |
1,680 px track, 1,400 px travel | Travel = track height − scroller height; the amplitudes spread over it. 300% gives 560 px and a parallax 2.5 times livelier; height: auto with content placed after the viewport (pulled up by a negative margin equal to its height) scrolls that content over the scene, as in the three scenes on this page. |
Frame height (CSS — .parallax-container and .parallax-viewport { height: 280px }) |
280 px, both | Change both together: the pinned viewport must be as tall as the scroller. A fixed value is mandatory on the scroller (with height: 100% under a parent with no height: travel 0, measured). Layers anchored at the bottom and stars in percentages: the scene redistributes itself. |
Sky and layer colors (CSS — .demo-preview { background }, .slr-parallax-bg, .parallax-mid::before, .parallax-fg) |
sky #0a0a0f, indigo rgba(99,102,241,.15–.25), violet rgba(139,92,246,.2–.3), ground rgba(15,15,26,.95) | The viewport is transparent: the sky is the frame's background. The layers are translucent gradients: on a light sky, raise their opacities (.4 to .6) or switch to solid colors. The ground, 95% opaque, also serves as a background for whatever you place at the bottom of the frame. |
Ridges (CSS — .slr-parallax-bg::before and ::after) |
280 × 140 px at left: −10%; 240 × 110 px at right: −5% | Border triangles: the width is border-left + border-right, the height border-bottom. 200 px borders make a 400 × 200 px ridge; left: 30% moves it. A third ridge needs one more element in the band. |
Stars and moon (HTML — eight .p-star; CSS — .p-star:nth-child(n), .parallax-moon) |
8 dots of 2 px (two of 3 px), opacities .3 to .8; 30 px moon at top 20 / right 30, 20 px halo | Add spans and their :nth-child rule (position in %, opacity); without a rule, a dot lands in the top-left corner. The moon shares parallaxStars with the stars; give it parallaxSlow to bring it closer, or no animation to pin it. |
“Scrollez pour le parallax” hint (HTML + CSS — .slr-scroll-hint, @keyframes hintBounce) |
8 px from the bottom of the frame, 11.2 px, white at 40%, 4 px bounce / 2 s | Anchored to .demo-preview (position: relative): without that frame, it drops to the bottom of the page. Hard-coded French text, 3.8:1 contrast: translate it, lighten it or delete the span. It is the effect's only autonomous motion. |
Fallback without animation-timeline (CSS — @supports not (animation-timeline: scroll())) |
animation: none on the five layers | Firefox and older browsers see the scene flat. To show them something else, replace the block: a fixed transform: translateY(…) per layer composes the view of your choice. |
FAQ
height: 100% under a parent with no definite height, it collapses to the height of its content, the travel is 0 and the timeline has nothing to follow. The scroller itself: the timeline is named on .parallax-container, so that is what must scroll, not the page; the wheel only reaches it when the pointer is over it, and an overflow: visible set by your CSS makes it inert. To drive the scene from the page scroll, replace the named timeline with animation-timeline: scroll(root) on the five layers, remove the container's overflow and keep the viewport sticky inside a tall enough section.parallaxFast goes from 120 to −30 px on a 60 px band anchored at the bottom. At rest, the band is 120 px below the edge: invisible. It surfaces at 700 px of travel (15 px visible), is whole at 1,050 px, then rises to −30: its 60 px occupy the 208–268 band of a 280 px viewport and the bottom 30 px show the frame background — measured identical at 390 and 1,280 px wide. That is the design as sold: the layers sink then rise, and the “assembled” composition is the mid-travel one, the same as the Firefox fallback. For a ground flush with the edge to the end, finish parallaxFast at translateY(0); to hide the gap without touching the amplitudes, make the ground 90 px tall (.parallax-fg { height: 90px }): at −30 it still covers the edge.scroll listener and six style writes per event on abstract shapes (a halo, three shapes rotated 180°, the word PARALLAXE enlarged and faded), with four global consts that limit the page to one instance; in exchange, it runs in Firefox. fx-0573 has not a single line of script: a named timeline, four @keyframes, a sticky viewport that never leaves the frame, as many instances as you want (measured: two copies, two independent timelines) — and nothing moves in Firefox. Its scene is a landscape drawn in CSS, not shapes to replace. fx-0592, like Parallax Layers CSS (fx-0548), is also pure CSS but with an anonymous animation-timeline: scroll() and layers that scroll with their section: the scene rises and leaves the frame. fx-0573 is the only one of the three where the frame stays pinned while the layers slide inside it.