Scroll-linked animations without a library
Fade-in-on-scroll used to mean IntersectionObserver, GSAP, or AOS.js. CSS can now trigger animations based on scroll position, zero JavaScript, smooth 60fps.
2const obs = new IntersectionObserver(
3 (entries) => {
4 entries.forEach(e => {
5 if (e.isIntersecting)
6 e.target.classList.add('visible');
7 });
8 }
9);
10document.querySelectorAll('.reveal')
11 .forEach(el => obs.observe(el));
2 from { opacity: 0; translate: 0 40px; }
3 to { opacity: 1; translate: 0 0; }
4}
5
6.reveal {
7 animation: reveal linear both;
8 animation-timeline: view();
9 animation-range: entry 0% entry 100%;
10}
11/* No JS. GPU-accelerated. */
GPU-accelerated
Runs on the compositor thread, 60fps guaranteed. No main-thread jank from JS observers or scroll handlers.
No JavaScript at all
Drop GSAP, AOS.js, or custom IntersectionObserver code. Entire scroll animation in 4 lines of CSS.
Reversible by default
Scroll back up and the animation reverses naturally. No need for "unobserve" or state management.
How it works
animation-timeline: view() binds a standard @keyframes animation to the element's visibility in the scroll port. As the element scrolls into view, the animation progresses from 0% to 100%.
animation-range: entry 0% entry 100% means the animation plays fully during the "entry" phase, from the moment the element's edge appears to when it's fully visible. You can also use exit, cover, or contain ranges.
Since this uses the browser's animation engine (compositor thread), it's inherently smoother than any JavaScript approach that mutates styles on the main thread.