The problem with AOS and ScrollReveal.js
Both libraries work well for simple cases. But they have the same fundamental design: scroll animation is configured via HTML data attributes — data-aos="fade-up" or data-sr-id. This means:
- Animations are scattered across your HTML — hard to trace, hard to change globally
- No type safety — a typo in a data attribute silently does nothing
- Data attributes mix concerns — presentation logic in HTML, not JavaScript
- Both add ~10–20 KB on top of your bundle, just for fade-in effects
- ScrollReveal.js is not actively maintained (last release: 2021)
scrollRevealfrom svg-scroll-draw is the JS-first alternative: all configuration lives in your JavaScript, it's fully typed, and it's part of a broader scroll animation platform — not a standalone one-trick library.
Zero-config: the default (fade up)
The most common scroll animation — fade up — requires zero configuration:
import { scrollReveal } from 'svg-scroll-draw/reveal';
// Fade up every .card on scroll — that's it
scrollReveal('.card');Compare that with AOS:
<!-- AOS: every element needs a data attribute -->
<div class="card" data-aos="fade-up">…</div>
<div class="card" data-aos="fade-up" data-aos-delay="100">…</div>
<div class="card" data-aos="fade-up" data-aos-delay="200">…</div>
<!-- Plus AOS init in your JS -->
AOS.init({ duration: 800, once: true });7 named presets
import { scrollReveal } from 'svg-scroll-draw/reveal';
scrollReveal('.card', { preset: 'fadeUp' }); // ↑ default
scrollReveal('.badge', { preset: 'fadeDown' }); // ↓
scrollReveal('.sidebar', { preset: 'fadeLeft' }); // ←
scrollReveal('.tooltip', { preset: 'fadeRight' }); // →
scrollReveal('.hero', { preset: 'scale' }); // scale up from 88%
scrollReveal('.panel', { preset: 'flip' }); // rotateX 20→0 (3D flip)
scrollReveal('.tile', { preset: 'flipX' }); // rotateY 20→0Custom from state
Full control — combine any properties with numeric values:
scrollReveal('.feature', {
from: {
opacity: 0,
y: 40, // translateY(40px) → 0
scale: 0.95, // scale(0.95) → 1
},
easing: 'ease-out',
stagger: 0.08,
once: true,
});
// 3D card flip
scrollReveal('.card', {
from: { opacity: 0, rotateX: 15, scale: 0.97 },
});
// Slide in from left with full opacity (no fade)
scrollReveal('.list-item', {
from: { x: -48 }, // translateX(-48px) → 0, no opacity change
});Stagger
The staggeroption offsets each element's trigger window — earlier elements in the list start animating sooner, later ones start a little further down the viewport. This creates a natural cascade without any per-element delay config:
// Cards cascade: card-0 animates first, then card-1, card-2…
scrollReveal('.pricing-card', {
preset: 'fadeUp',
stagger: 0.12,
easing: 'ease-out',
});
// Very subtle stagger for a large list
scrollReveal('.testimonial', {
preset: 'scale',
stagger: 0.06,
});`}React usage
'use client';
import { useEffect } from 'react';
import { scrollReveal } from 'svg-scroll-draw/reveal';
export function FeaturesSection() {
useEffect(() => {
const instance = scrollReveal('.feature-card', {
preset: 'fadeUp',
stagger: 0.1,
once: true,
});
return () => instance.destroy();
}, []);
return (
<section>
{features.map(f => (
<div key={f.id} className="feature-card">
{f.content}
</div>
))}
</section>
);
}Migration table
| AOS / ScrollReveal.js | scrollReveal |
|---|---|
| data-aos="fade-up" | scrollReveal('.el') // default |
| data-aos="fade-left" | scrollReveal('.el', { preset: 'fadeLeft' }) |
| data-aos="zoom-in" | scrollReveal('.el', { preset: 'scale' }) |
| data-aos="flip-up" | scrollReveal('.el', { preset: 'flip' }) |
| data-aos-delay="100" | stagger: 0.1 |
| data-aos-duration="800" | trigger: { start: 'top 85%', end: 'top 50%' } |
| data-aos-once="true" | once: true (default) |
| AOS.init({ once: true }) | scrollReveal('.el', { once: true }) |
| AOS.refresh() | instance.destroy(); scrollReveal(...) |
Full API
import { scrollReveal } from 'svg-scroll-draw/reveal';
const instance = scrollReveal(
'.card', // CSS selector, NodeList, or Element[]
{
preset: 'fadeUp', // 'fadeUp' | 'fadeDown' | 'fadeLeft' | 'fadeRight'
// | 'scale' | 'flip' | 'flipX'
from: { // custom start state (overrides preset)
opacity: 0,
x: 0, y: 0,
scale: 1,
rotate: 0,
rotateX: 0, rotateY: 0,
},
stagger: 0.08, // viewport-% offset per element (default: 0.08)
easing: 'ease-out', // any easing name or custom function
once: true, // freeze at max progress (default: true)
trigger: { // override default trigger window
start: 'top 88%',
end: 'top 53%',
},
onEnter: () => {}, // fires when first element enters view
onLeave: () => {}, // fires when last element leaves view
}
);
instance.destroy(); // removes all animations, restores original stylesscrollReveal for the common case — preset animations on a group of elements. Use scrollAnimateGroup when you need precise per-element control over different CSS properties with shared trigger timing.