How to Implement Scroll-Triggered Animations with Intersection Observer in Next.js: A Production-Ready Guide
The woosal1337/blog repository provides a reusable Reveal component that leverages the native Intersection Observer API to animate elements when they scroll into view, featuring staggered delays and automatic reduced-motion accessibility support.
Scroll-triggered animations enhance user engagement without sacrificing initial page load performance. The woosal1337/blog repository demonstrates an elegant implementation using a client-side React component that wraps the browser's Intersection Observer API within a Next.js 14 App Router architecture. This guide examines the Reveal component located in components/ds/reveal.tsx and provides practical patterns for integrating scroll-based animations across your application.
Core Architecture of the Reveal Component
The Reveal component is a client-side only solution (declared with "use client") that encapsulates intersection detection logic. Located at components/ds/reveal.tsx, it combines React refs, state management, and the native Intersection Observer API to toggle visibility classes when elements enter the viewport.
Props Interface and Configuration
The component accepts a flexible property set defined by the RevealProps interface (lines 8-13). The available props include:
children: The React elements to animatedelay: An optional index (0-3) mapping to predefined animation delays (0ms, 50ms, 200ms, 400ms)immediate: A boolean flag to bypass the observer and show content instantlyclassName: Additional CSS classes for custom styling
Intersection Observer Implementation
The core detection logic resides in a useEffect hook (lines 33-40). The component initializes a ref pointing to a wrapper div and maintains a shown state (lines 22-24). When the observed element intersects with the viewport, the callback updates the state and immediately calls disconnect() to prevent unnecessary re-observation.
Accessibility and Reduced Motion
Before initializing the observer, the component checks for prefers-reduced-motion using window.matchMedia (lines 29-32). If the user prefers reduced motion, the component sets shown to true immediately, ensuring content remains accessible without triggering animations.
Animation Styling and Delays
The component toggles between CSS states through conditional class application (lines 49-52). The reveal-hidden class applies the initial hidden state, while reveal-play triggers the visible animation state.
Staggered entrance effects are controlled via the delay prop, which maps to specific millisecond values applied through inline styles (lines 53-54):
0: 0ms (no delay)1: 50ms2: 200ms3: 400ms
These values allow sequential animation of list items without complex coordination logic.
Usage Patterns in Next.js Pages
The repository demonstrates three primary implementation patterns across different application routes.
Immediate Reveal for Above-the-Fold Content
For critical content that should appear instantly without waiting for a scroll trigger, use the immediate prop. This pattern appears in app/(website)/projects/page.tsx (lines 32-45):
import { Reveal } from "@/components/ds/reveal";
<Reveal immediate className="mb-8">
<h2 className="text-2xl">Welcome to My Projects</h2>
</Reveal>
Staggered Grid Animations
For lists requiring sequential entrance effects, map the delay prop based on the item index. The projects page (lines 48-69) implements alternating delays:
{projects.map((project, index) => (
<Reveal
key={project.slug}
delay={index % 2 === 0 ? 1 : 2}
>
<ProjectCard data={project} />
</Reveal>
))}
Mixed Content Strategies
The blog page at app/(website)/blog/page.tsx (lines 72-111) combines immediate reveals for section headings with delayed reveals for post listings, creating a hierarchical visual flow:
<Reveal immediate className="mb-6">
<p className="prose">Some introductory paragraph.</p>
</Reveal>
{posts.map((post, i) => (
<Reveal key={post.slug} delay={i % 2 === 0 ? 1 : 2}>
<PostPreview data={post} />
</Reveal>
))}
Step-by-Step Integration Guide
To add scroll-triggered animations to your own Next.js components:
-
Import the component from your design system path:
import { Reveal } from "@/components/ds/reveal"; -
Wrap target elements with appropriate props:
<Reveal delay={0}> <YourElement /> </Reveal> -
Configure Tailwind CSS (or your styling solution) to define the
reveal-hiddenandreveal-playclasses with appropriate transitions or keyframes.
Key reference files in the woosal1337/blog repository include:
components/ds/reveal.tsx: Core implementation with Intersection Observer logicapp/(website)/projects/page.tsx: Staggered list animation examplesapp/(website)/blog/page.tsx: Mixed immediate and delayed reveal patternsapp/(website)/about/page.tsx: Section heading animationsapp/(website)/videos/page.tsx: Thumbnail grid animations
Summary
- The
Revealcomponent incomponents/ds/reveal.tsxprovides a complete, reusable Intersection Observer implementation for Next.js applications - It respects accessibility preferences by detecting
prefers-reduced-motionbefore executing animations - Supports four predefined stagger tiers (0ms, 50ms, 200ms, 400ms) via the
delayprop index - Uses
"use client"directive to ensure safe access towindowand browser APIs after hydration - Automatically disconnects the observer after triggering to optimize memory usage
Frequently Asked Questions
Does this work with Next.js Server Components?
No, the Reveal component requires client-side execution because it depends on window, IntersectionObserver, and matchMedia APIs. The source code explicitly declares "use client" at the top of components/ds/reveal.tsx to ensure the component only renders after hydration, preventing server-client mismatches.
How do I customize the animation duration or easing?
The component controls visibility timing through CSS class toggling (reveal-hidden and reveal-play) rather than inline styles. To modify animation characteristics, define custom keyframes and transition properties in your global CSS or Tailwind configuration. The component handles only the trigger timing, not the specific animation curves.
Can I use custom delay values outside the 0-3 index range?
The current implementation maps indices to fixed millisecond values (0ms, 50ms, 200ms, 400ms) in the inline style logic at lines 53-54. To support arbitrary delay values, you would need to modify the RevealProps interface to accept a number directly and update the style assignment logic to use that value verbatim.
Why does the observer disconnect immediately after the first trigger?
According to the implementation at lines 33-40, the observer calls disconnect() immediately upon detecting an intersection. This design prevents memory leaks and eliminates unnecessary callback executions once the element has been revealed, as the animation only needs to play once per element.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →