The .demo-preview frame (280 px, overflow: hidden, #0a0a0f background, white text, sans-serif) holds the .sdc-scroll-container scrolling box (100% × 280px, overflow-y: auto, scroll-behavior: smooth), the .sb-fadeup-content tray (900 px, padding: 60px 20px, flex column with gap: 24px) and five .sb-fadeup-box blocks — dark cards with 24 px of padding, one h4 and one p each. Measured in Chromium 151, blank page: 86 px blocks placed at 60, 170, 280, 390 and 500 px, 620 px scroll range. The “Scroll ↓” hint (.sdc-scroll-hint) anchors to the frame 8 px from the bottom and pulses from .3 to .7 every 2 s. The JavaScript field is empty.
The whole mechanism sits on .sb-fadeup-box: opacity: 0, transform: translateY(60px), animation: sbFadeUp linear both, animation-timeline: view(), animation-range: entry 0% entry 50%. view() replaces the clock with the block's position inside the nearest scroll container, here .sdc-scroll-container. The entry range runs from the moment the block's top edge crosses the bottom of the box (0%) to the moment the block is entirely inside (100%); entry 0% entry 50% keeps only the first half: for an 86 px block, the animation lives on 43 px of scrolling. The single keyframe, to { opacity: 1; transform: translateY(0) }, and the both fill freeze the start state before the range and the end state after it. Measured: at rest, blocks 1 and 2, already inside the box, are at 1; block 3, whose top edge sits right on the bottom edge, is at 0 — 0.93 and 4.2 px too low at 40 px of scrolling, 1 and 0 at 80. Block 4 enters at 110 px: 0.23 at 120, 1 at 160; block 5 enters at 220: 0.47 at 240, 1 at 280. Back to the top: blocks 3 to 5 return to 0, the timeline reads both ways.
The value of the effect lies in what it does not do: no :nth-child, no animation-delay, no counter. Each block carries its own timeline, set on its own height; three or three hundred entries obey the same rule, and a block added afterwards animates too (measured: a sixth block inserted by script, tray at height: auto, 0 → 1 between 300 and 400 px of scrolling, nothing to re-initialize). A delay in milliseconds has no effect on a scroll timeline (measured with animation-delay: 200ms: same values), which makes the cascades of Stagger Cards (fx-0540) and Stagger Reveal CSS (fx-0552) pointless. For a block taller than the box, the entry range ends when the block covers the box entirely: measured, a 600 px block in the 280 px box animates over 140 px, half the box — the rule is half of the smaller of the two, block or box. The 900 px tray is a demo fixture (the blocks end at 586 px, beyond 306 px of scrolling nothing new enters): remove that height, and if the list sits in the document flow, view() takes the viewport as its box (measured: the same 43 px per block).
CSS only, zero dependencies, zero JavaScript. Two recent properties carry everything: animation-timeline: view() and animation-range with the named entry range — Chrome and Edge 115 (July 2023), Safari 26 (September 2025; measured in WebKit 26.5: same values as Chromium). Firefox: no version. Measured in Firefox 153: CSS.supports('animation-timeline: view()') returns false, both declarations are ignored and animation: sbFadeUp linear both runs on the document clock with a 0 s duration, finished at load: all five blocks are at opacity: 1 and translateY(0) from the first frame, before and after scrolling, the box scrolls its 620 px and the hint pulses. A Firefox visitor sees a flat list, complete and readable — nothing hidden, nothing moving. No @supports is needed because the final keyframe is the visible state; if your keyframes change, add @supports not (animation-timeline: view()) { .sb-fadeup-box { animation: none; opacity: 1; transform: none } }.