Split Screen Sticky
A visual pinned 10 px from the top whose SVG icon turns 360° over the 720 px travel — scroll(), 0.5° per pixel, reversible with the gesture — while three blocks reveal themselves through view(). CSS only, 2.7 KB.
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 five levels: the .demo-preview frame (centered flex, min-height: 300px, position: relative, #0a0a0f background, white text), the .sdc-scroll-container box (100% × 280px, overflow-y: auto, scroll-behavior: smooth, 4 px bar through ::-webkit-scrollbar), the .ss-split-content track (1,000 px, padding: 12px, flex column), the .ss-split-visual band (full width × 100 px, #1a1a3e → #2d1b69 gradient, position: sticky; top: 10px; z-index: 2) with its 60 px white-stroke SVG, then the .ss-split-items stack and its three 120 px .ss-split-item blocks, laid out from 128 to 520 px. Below the box, the .sdc-scroll-hint label “Scrollez ↓” pulses between .3 and .7 every 2 s. Travel: 1000 − 280 = 720 px. 2.7 KB of CSS, empty JavaScript field, no site variable, no external resource, no console error; the only foreign leftover is an inline style="font-family: Inter…" on the three h4.
The rotation is one rule: .ss-split-visual svg { animation-name: ssRotateSvg; animation-timeline: scroll(); animation-range: normal; animation-duration: auto; animation-timing-function: linear; animation-fill-mode: none }, with @keyframes ssRotateSvg { to { transform: rotate(360deg) } }. scroll() with no argument takes the nearest scroller — the 280 px box, not the page — and normal spreads the animation over its whole travel: progress = scrollTop ÷ 720, i.e. 0.5° per pixel. Measured in Chromium 151 with 120 px wheel steps: 60°, 120°, 180°, 240°, 300°, then transform: none at 720 px, which is 360°. Three steps back give 180°, then 0°: nothing turns without the reader, and the icon retraces its steps with them. sticky; top: 10px holds the band 12 px from the top of the box at rest (the track's padding), 10 px from the first pixel, and all the way down — its containing block is the whole track.
The blocks follow a second timeline: .ss-split-item { animation-name: sbFadeUp; animation-timeline: view(); animation-range: entry entry 50%; animation-fill-mode: both }, from opacity: 0; transform: translateY(20px) to 1 and translateY(0). view() tracks the block's own position in the box: the fade starts when its top touches the bottom edge and ends once half the block (60 px) is in. At rest, block 1 (128 → 248 px) is fully inside the box: opacity 1. Block 2 starts at 264 px, 16 px before the edge: 13% in, 27% of its range — opacity .27, offset 14.7 px, measured; it reaches 1 after 44 px of scrolling. Block 3 (400 px) stays at 0 until 120 px and is 1 from 180. Every block that climbs passes under the band (z-index: 2). At 520 px, the last block has left the box: the last 200 px of travel (28%, 100° of rotation) show only the pinned band over the empty background.
Accessibility
- prefers-reduced-motion is absent from the code: measured under emulation in Chromium, nothing changes — 60° at 120 px, block 2 at .27, pulsing hint,
scroll-behavior: smooth. The motion is driven by the reader (interaction-triggered animation, WCAG 2.3.3) and nothing is hidden, but add@media (prefers-reduced-motion: reduce) { .ss-split-visual svg, .ss-split-item, .sdc-scroll-hint { animation: none } .ss-split-item { opacity: 1; transform: none } .sdc-scroll-container { scroll-behavior: auto } }—opacityandtransformare essential:animation: nonealone leaves the blocks invisible. - Keyboard: measured, Tab focuses the box in Chromium and Firefox with no
tabindex— Down arrow moves 40 px, i.e. 20° (51 px in Firefox), PageDown 245 px, End goes to 720; WebKit does not focus it (focus stays onbody). Addtabindex="0", anaria-labeland a:focus-visiblestyle. On touch, scrolling is native (no listener);overscroll-behavior: containkeeps the page from being dragged along at the end of the travel. - Contrast on the blocks (4% white over
#0a0a0f): headings 18.4:1; paragraphs at 50% white 5.3:1 at 11.5 px; lists at 40% white 3.8:1, below the 4.5:1 threshold — raise them torgba(255,255,255,.55). The “Scrollez ↓” hint is 30% white and its opacity pulses from .3 to .7 — the two multiply: 2.6:1 static, 1.2:1 to 1.8:1 under the pulse, at 10.4 px; decorative, give itaria-hidden="true". A settled block is never semi-transparent: the fade only concerns its entry. - Screen readers and frame: the SVG has neither
<title>noraria-hidden— give it one or the other depending on whether it carries meaning. Blocks atopacity: 0are still read in order. Threeh4with no h2/h3 above: use the level that follows your heading. The demo texts are French with missing accents (“Renforcee”, “Protegez vos donnees”). Paste the HTML with its frame: without.demo-preview, measured, the box has no background left — white text on a white page — and the hint drops to the bottom of the viewport.
Browser compatibility
CSS only: animation-timeline: scroll() and view(), animation-range, animation-duration: auto — all three arrived together in Chrome and Edge 115, Safari 26 (iOS 26). Firefox: no version (module behind layout.css.scroll-driven-animations.enabled, off by default). The rest is classic: position: sticky, flexbox, gradient, @keyframes, scroll-behavior. Zero dependency, zero JavaScript. Measured in WebKit 26.5: same values as Chromium (60° at 120 px, block 2 at .27, 360° at 720).
Measured in Firefox 153, sold code, blank page: CSS.supports('animation-timeline: scroll()') returns false, animation-duration: auto is rejected and computes to 0s — both animations fall back to the document clock and last 0 s. The SVG keeps transform: none (fill-mode: none): the icon does not turn, at any position. The blocks take their final frame (fill-mode: both): opacity 1, translateY(0) from load — all three read flat, the third included, before any scrolling. The band stays pinned, the box scrolls with its native bar. No @supports fallback in the code, and there is nothing to catch: the keyframes' final state is the readable state. To spell it out: @supports not (animation-timeline: scroll()) { .ss-split-item { animation: none; opacity: 1; transform: none } }. Same rendering in Chrome before 115 and Safari before 26.
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="ss-split-content">
<div class="ss-split-visual">
<svg>…</svg>
</div>
<div class="ss-split-items">
<div class="ss-split-item">
<h4>…</h4>
<p>…</p>
<ul><li>…</li><li>…</li><li>…</li></ul>
</div>
<div class="ss-split-item">…</div>
<div class="ss-split-item">…</div>
</div>
</div>
</div>
<span class="sdc-scroll-hint">…</span>
</div>
The full HTML + CSS + JS of Split Screen Sticky, 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 |
|---|---|---|
Travel (CSS — .ss-split-content { height: 1000px }) |
1,000 px (travel 720, 0.5° per pixel) | Travel = track − box, and the rotation always spreads over all of it. 570 px → travel 290, 1.24° per pixel, text and rotation end together (last block parked under the band, measured). height: auto with your own blocks: the travel follows the content. |
Box height (CSS — .sdc-scroll-container { height: 280px }) |
280 px | At 400 px (measured): travel 600, blocks 1 and 2 settled from load, block 3 at 0. Remove height and overflow-y to let the page scroll itself: scroll() and view() then climb to the viewport. |
Angle (CSS — @keyframes ssRotateSvg { to { transform: rotate(360deg) } }) |
360° (one turn over the travel) | 720deg two turns, -360deg the other way, 180deg a half turn — measured: the icon stays at 180° at the end of the travel despite fill-mode: none. Add scale(1.2) in the same frame to turn and grow together. |
Fade range (CSS — .ss-split-item { animation-range: entry entry 50% }) |
entry 0% → entry 50% (60 of the 120 px) | entry entry 100% spreads the fade over the whole entry (block 2 at .13 at rest instead of .27, measured); entry 0% entry 30% tightens it (.44). Keep an end before entry 100%: the block must be sharp before the eye reaches it. |
Fade amplitude (CSS — .ss-split-item { opacity: 0; transform: translateY(20px) }) |
0 → 1, 20 px rise | The start is in the rule, the arrival in @keyframes sbFadeUp. translateY(40px) emphasises the rise; translateY(0) scale(.96) replaces it with a slight growth. |
Pinning (CSS — .ss-split-visual { top: 10px; height: 100px; z-index: 2 }) |
10 px from the top, 100 px tall, above the blocks | top: 0 glues the band to the edge; height shifts the first block (12 px + height + 16 px); without z-index: 2, the climbing blocks would pass over the band instead of sliding under it. |
Visual (HTML — <svg width="60" height="60">; CSS — .ss-split-visual svg { stroke: #fff; stroke-width: 1.5 }) |
60 px, 1.5 white stroke, three layers | Replace the content of the <svg> (logo, pictogram): every svg in the band turns, an <img> does not — widen the selector (.ss-split-visual svg, .ss-split-visual img). Text placed next to the SVG inside the band stays fixed: the three scenes on this page do that. |
FAQ
view() measures each block's entry into the 280 px box and entry entry 50% ends the fade once 60 px have passed. At load, block 2 starts at 264 px: 16 px already in, 13% of its height, 27% of the range — opacity .27, measured. Block 3 starts at 400 px, 120 px below the edge: 0 until scrolling brings it there, 1 at 180 px. That is the geometry of the sold code, nothing is broken. For a clean rest state: a 400 px box (blocks 1 and 2 at 1, measured), a shorter range (entry 0% entry 30%: .44), or padding-top: 32px on .ss-split-items — block 2 then starts at 280 px, just outside the box, at 0..ss-split-content { height: 570px } — travel 290, 1.24° per pixel, the third block stops parked under the band (110 → 230 px, measured); height: auto with enough blocks for the travel to come from the content (the scenes on this page: 516 px for five blocks, 376 for four); or keep 1,000 px and add three blocks — at the end of the travel, the sixth stops at 88 px, its top slid 22 px under the band (measured)..sdc-scroll-container box, a band stuck at 10 px, panels on view() over entry 0% entry 50% — but not the top timeline. In fx-0585 (free), ssImgChange on scroll(nearest) animates the band's background in steps: four gradients held over 0–20%, 25–45%, 50–70% and 75–100% of the 920 px travel, with a 5% transition between two — four discrete states, a fixed “01”, no image. Here the timeline animates a continuous rotation of the SVG illustration: 0.5° per pixel, every position has its angle, and scrolling back undoes it degree by degree. fx-0585 to signal sections (one colour = one step); fx-0589 for an object the reader handles with the gesture — icon, gear, pictogram — that must come back with them.