We need to talk about scrollytelling. For years, the standard advice for high-end web experiences has been “just use GSAP or a scroll listener.” While those tools are powerful, they are fundamentally tethered to the main thread. I’ve seen countless projects—even expensive ones for big agencies—choke on mobile because the developer tried to animate hundreds of characters using a JavaScript race condition disguised as a scroll event. If the main thread is busy calculating positions, your frames drop, and your “immersive experience” becomes a janky mess.
The messy reality is that JavaScript performance is often the bottleneck when we scale animations. However, with the introduction of CSS sibling-index() and the scroll-timeline API, we can finally move the heavy lifting to the compositor. Specifically, we can recreate complex effects like a “text vortex” where every single character is animated individually, without incurring the astronomical performance impact of traditional methods.
Why CSS sibling-index() Changes the Game
Previously, to stagger an animation based on an element’s position, you had to loop through the DOM with JS or hardcode nth-child rules in your stylesheet. That is a maintenance nightmare. Consequently, many devs just reached for JS libraries because it allowed them to base styling on element indexes easily. Furthermore, native CSS sibling-index() allows the browser to handle these calculations internally, which is a massive win for performance.
If you’ve been following my previous notes on better CSS bar charts, you know I’m a pragmatist. I don’t use new features just because they are shiny; I use them because they solve the “mobile jank” problem that haunts modern scrollytelling.
Confession: We Still Need a Little Script
To animate characters, we first have to tokenize the string. While I’d love to say CSS can handle this, we still need a tiny bit of JS to split the text into divs. I prefer using the GSAP SplitText plugin because it handles accessibility (aria-labels) out of the box, even if we aren’t using the actual GSAP animation engine.
const el = document.querySelector(".vortex");
el.innerHTML = el.innerHTML.replaceAll(/\s/g, '⠀');
new SplitText(".title", { type: "chars", charsClass: "char" });
I refactored this recently for a client who was struggling with a broken checkout page—the culprit was actually a global scroll listener on their “pretty” footer. By moving to this hybrid approach, we fixed the footer without breaking the UI. Therefore, the key is using JS for the markup and CSS for the motion.
Building the Vortex with Pure CSS
Once your characters are wrapped in .char divs, the CSS sibling-index() and sibling-count() functions do the heavy lifting. We use a simple mathematical formula to calculate the radius and rotation of each character based on its index relative to the total count.
.vortex {
position: fixed;
animation-timeline: scroll();
.char {
/* The Magic Sauce */
--radius: calc(10vh - (7vh / sibling-count() * sibling-index()));
--rotation: calc((360deg * 3 / sibling-count()) * sibling-index());
transform: rotate(var(--rotation))
translateY(calc(-2.9 * var(--radius)))
scale(calc(.4 - (.25 / sibling-count() * sibling-index())));
animation-name: fade-in;
animation-range-start: calc(90% / sibling-count() * sibling-index());
animation-timeline: scroll();
}
}
This approach effectively kills framework bloat. By linking the animation to the scroll-timeline, the browser only updates the visuals when the scroll position actually changes, and it does so off the main thread.
Look, if this scrollytelling stuff is eating up your dev hours, let me handle it. I’ve been wrestling with WordPress since the 4.x days.
Ahmad’s Takeaway
We are entering an era where the “it’s too complex for CSS” excuse is dying. If you are still building scroll animations that rely on window.addEventListener('scroll'), you are creating technical debt for your future self. Start experimenting with these native functions now. Even if you have to provide a video fallback for Firefox, the performance gains on Chromium-based browsers (and eventually all of them) are worth the refactor. Ship it.