Zero-dependency, SSR-safe scroll reveal animations for React. ~3 KB gzipped.
Built for Next.js App Router. Works with any React 18+ app.
Framer Motion is 36 KB+ gzipped. You need fade-up-on-scroll. This library is 3 KB, has zero dependencies, and is SSR-safe out of the box.
| react-stagger-reveal | Framer Motion | AOS | react-intersection-observer | |
|---|---|---|---|---|
| Size | ~3 KB | 36 KB+ | 14 KB | ~1 KB |
| Zero deps | Yes | No | No | Yes |
| SSR safe | Yes | Yes | No | Yes |
| Stagger children | Yes | Yes | No | No |
| CSS only | Yes | No (JS anim) | No | N/A (observer only) |
| Animations | Reveal / fade | Full suite | Reveal | None |
npm install react-stagger-revealyarn add react-stagger-revealpnpm add react-stagger-revealimport { StaggerOnView, FadeUp } from "react-stagger-reveal";
export function Features() {
return (
<StaggerOnView>
<FadeUp><h2>Fast</h2></FadeUp>
<FadeUp><h2>Simple</h2></FadeUp>
<FadeUp><h2>Lightweight</h2></FadeUp>
</StaggerOnView>
);
}Three lines. No configuration. Children fade up one by one as they scroll into view.
Animate children sequentially when the container scrolls into the viewport.
<StaggerOnView margin="-100px" staggerDelay={100} initialDelay={200}>
<FadeUp><Card /></FadeUp>
<FadeUp><Card /></FadeUp>
<FadeUp><Card /></FadeUp>
</StaggerOnView>| Prop | Type | Default | Description |
|---|---|---|---|
as |
ElementType |
"div" |
HTML element or component to render |
margin |
string |
"-80px" |
IntersectionObserver rootMargin |
once |
boolean |
true |
Only trigger once |
staggerDelay |
number |
120 |
ms between each child |
initialDelay |
number |
150 |
ms before the first child |
className |
string |
-- | CSS class name(s) |
Animate children sequentially on mount (no scroll trigger).
<StaggerOnMount>
<FadeUp><NavItem /></FadeUp>
<FadeUp><NavItem /></FadeUp>
</StaggerOnMount>| Prop | Type | Default | Description |
|---|---|---|---|
as |
ElementType |
"div" |
HTML element or component to render |
staggerDelay |
number |
120 |
ms between each child |
initialDelay |
number |
150 |
ms before the first child |
className |
string |
-- | CSS class name(s) |
Child of a stagger container. Fades up with an auto-calculated delay based on its sibling index.
<StaggerOnView>
<FadeUp as="li" distance={30} duration={500}>
<span>Item</span>
</FadeUp>
</StaggerOnView>| Prop | Type | Default | Description |
|---|---|---|---|
as |
ElementType |
"div" |
HTML element or component to render |
distance |
number |
20 |
translateY distance in px |
duration |
number |
700 |
Transition duration in ms |
easing |
string |
"cubic-bezier(0.25, 0.1, 0.25, 1)" |
CSS timing function |
staggerDelay |
number |
120 |
Override parent stagger delay |
initialDelay |
number |
150 |
Override parent initial delay |
className |
string |
-- | CSS class name(s) |
Standalone fade-in on scroll. No stagger container needed.
<FadeOnView duration={500}>
<img src="/hero.jpg" alt="Hero" />
</FadeOnView>| Prop | Type | Default | Description |
|---|---|---|---|
as |
ElementType |
"div" |
HTML element or component to render |
duration |
number |
600 |
Transition duration in ms |
easing |
string |
"cubic-bezier(0.25, 0.1, 0.25, 1)" |
CSS timing function |
margin |
string |
"-80px" |
IntersectionObserver rootMargin |
once |
boolean |
true |
Only trigger once |
className |
string |
-- | CSS class name(s) |
Standalone fade-up on scroll. Combines vertical translation with opacity.
<FadeUpOnView distance={30} duration={800}>
<section>About us</section>
</FadeUpOnView>| Prop | Type | Default | Description |
|---|---|---|---|
as |
ElementType |
"div" |
HTML element or component to render |
distance |
number |
20 |
translateY distance in px |
duration |
number |
700 |
Transition duration in ms |
easing |
string |
"cubic-bezier(0.25, 0.1, 0.25, 1)" |
CSS timing function |
margin |
string |
"-80px" |
IntersectionObserver rootMargin |
once |
boolean |
true |
Only trigger once |
className |
string |
-- | CSS class name(s) |
Fades in after a fixed time delay. Useful after hero animations or loading states.
<DelayedFade delay={2}>
<p>Appears after 2 seconds</p>
</DelayedFade>| Prop | Type | Default | Description |
|---|---|---|---|
as |
ElementType |
"div" |
HTML element or component to render |
delay |
number |
1.5 |
Delay in seconds |
duration |
number |
800 |
Transition duration in ms |
easing |
string |
"ease" |
CSS timing function |
className |
string |
-- | CSS class name(s) |
Low-level hook that reports whether an element is in the viewport.
import { useInView } from "react-stagger-reveal";
function MyComponent() {
const { ref, inView } = useInView("-100px");
return (
<div ref={ref}>
{inView ? "Visible!" : "Not yet..."}
</div>
);
}| Param | Type | Default | Description |
|---|---|---|---|
margin |
string |
"-80px" |
IntersectionObserver rootMargin |
once |
boolean |
true |
Stop observing after first hit |
Returns: { ref: RefObject<HTMLElement | null>, inView: boolean }
- SSR: Renders children fully visible. Crawlers and screen readers see all content immediately.
- Hydration: After React hydrates, elements are hidden (opacity 0, translateY offset).
- Trigger:
IntersectionObserverdetects scroll visibility, orrequestAnimationFramefires on mount. - Stagger: The parent container sets a
data-stagger-mountedattribute. EachFadeUpchild watches for this attribute viaMutationObserverand calculates its delay from its sibling index. - Animate: Pure CSS transitions handle the actual animation (opacity + transform). No JavaScript animation loop. No layout thrashing.
This architecture means:
- Content is always in the DOM (no conditional rendering, no
display: none) - Animations run on the compositor thread (opacity + transform are GPU-accelerated)
- The library is tree-shakeable -- import only what you use
Every component accepts an as prop for semantic HTML:
<StaggerOnView as="ul">
<FadeUp as="li">First</FadeUp>
<FadeUp as="li">Second</FadeUp>
<FadeUp as="li">Third</FadeUp>
</StaggerOnView>
<FadeUpOnView as="section" className="hero">
<h1>Welcome</h1>
</FadeUpOnView>prefers-reduced-motion: When the user has enabled reduced motion in their OS settings, all animations are skipped. Elements render immediately visible with no transitions.- Always in the DOM: Content is never conditionally rendered. Screen readers and crawlers always have access.
- SSR visible: During server-side rendering, all content renders at full opacity so that non-JS environments see everything.
- No layout shift: Animations use
opacityandtransformonly, which do not cause layout reflow.
The package includes "use client" directives. It works out of the box with Next.js App Router -- no extra configuration needed.
// app/page.tsx (Server Component)
import { Features } from "./features";
export default function Home() {
return <Features />;
}
// app/features.tsx (automatically a Client Component via the import)
import { StaggerOnView, FadeUp } from "react-stagger-reveal";
export function Features() {
return (
<StaggerOnView>
<FadeUp><div>Feature 1</div></FadeUp>
<FadeUp><div>Feature 2</div></FadeUp>
</StaggerOnView>
);
}Works in all browsers that support IntersectionObserver (Chrome 51+, Firefox 55+, Safari 12.1+, Edge 15+). In older browsers, elements render immediately visible (graceful degradation, not a blank page).
MIT -- Spotbo, Inc.