# How to Implement Scroll-Triggered Animations with Intersection Observer in Next.js: A Production-Ready Guide

> Implement scroll-triggered animations in Next.js using the Intersection Observer API. Discover a production-ready reusable component with staggered delays and accessibility support.

- Repository: [Ege Chelebi/blog](https://github.com/woosal1337/blog)
- Tags: how-to-guide
- Published: 2026-08-06

---

**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`](https://github.com/woosal1337/blog/blob/main/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`](https://github.com/woosal1337/blog/blob/main/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 animate
- `delay`: 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 instantly
- `className`: 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`: 50ms
- `2`: 200ms
- `3`: 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):

```tsx
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:

```tsx
{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:

```tsx
<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:

1. **Import the component** from your design system path:

   ```tsx
   import { Reveal } from "@/components/ds/reveal";
   ```

2. **Wrap target elements** with appropriate props:

   ```tsx
   <Reveal delay={0}>
     <YourElement />
   </Reveal>
   ```

3. **Configure Tailwind CSS** (or your styling solution) to define the `reveal-hidden` and `reveal-play` classes with appropriate transitions or keyframes.

Key reference files in the woosal1337/blog repository include:
- [`components/ds/reveal.tsx`](https://github.com/woosal1337/blog/blob/main/components/ds/reveal.tsx): Core implementation with Intersection Observer logic
- `app/(website)/projects/page.tsx`: Staggered list animation examples
- `app/(website)/blog/page.tsx`: Mixed immediate and delayed reveal patterns
- `app/(website)/about/page.tsx`: Section heading animations
- `app/(website)/videos/page.tsx`: Thumbnail grid animations

## Summary

- The `Reveal` component in [`components/ds/reveal.tsx`](https://github.com/woosal1337/blog/blob/main/components/ds/reveal.tsx) provides a complete, reusable Intersection Observer implementation for Next.js applications
- It respects accessibility preferences by detecting `prefers-reduced-motion` before executing animations
- Supports four predefined stagger tiers (0ms, 50ms, 200ms, 400ms) via the `delay` prop index
- Uses `"use client"` directive to ensure safe access to `window` and 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`](https://github.com/woosal1337/blog/blob/main/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.