# Animation Strategy for a Next.js Editorial Site: CSS-First Reveals with IntersectionObserver

> Discover a CSS-first animation strategy for your Next.js editorial site. Use IntersectionObserver and Tailwind for lightweight, accessible reveals without heavy JS.

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

---

**The optimal animation strategy for a Next.js editorial site combines CSS keyframes, the native IntersectionObserver API, and Tailwind utilities to create lightweight, staggered reveals that respect accessibility preferences without adding heavy JavaScript libraries.**

Building a performant editorial experience requires animations that enhance readability without distracting from content. According to the woosal1337/blog source code, you can implement a complete animation pipeline using only native browser APIs and CSS, keeping your bundle size minimal while supporting reduced-motion preferences.

## Core Architecture: The Reveal Component

The foundation of this animation strategy sits in [`components/ds/reveal.tsx`](https://github.com/woosal1337/blog/blob/main/components/ds/reveal.tsx), which wraps elements to trigger entrance animations only when they scroll into view. Unlike heavy animation libraries, this component leverages the browser-native **IntersectionObserver** API to toggle animation classes efficiently.

### IntersectionObserver Implementation

The component creates an observer instance that watches for the element to intersect with the viewport before adding the `reveal-play` class:

```tsx
// From components/ds/reveal.tsx (lines 33-40)
useEffect(() => {
  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) {
      setIsVisible(true);
      observer.disconnect();
    }
  }, { threshold: 0.1 });
  
  if (ref.current) observer.observe(ref.current);
}, []);

```

This approach prevents off-screen animations from consuming CPU cycles, a critical optimization for long-form editorial content with many animated elements.

### Staggered Delay Logic

Editorial layouts often require cascading reveals for lists or grids. The component accepts a `delay` prop that maps to predefined timing values:

```tsx
// From components/ds/reveal.tsx (lines 6-13, 53-54)
const DELAYS = [0, 50, 200, 400] as const;

interface RevealProps {
  delay?: 0 | 1 | 2 | 3;
  children: React.ReactNode;
}

// Applied via inline style
style={{ animationDelay: `${DELAYS[delay]}ms` }}

```

By applying `animationDelay` inline rather than generating dynamic CSS classes, the component maintains compatibility with Tailwind's utility-first architecture while allowing precise timing control.

## Global Keyframe Definitions

All animations are defined in [`app/globals.css`](https://github.com/woosal1337/blog/blob/main/app/globals.css) using Tailwind's `@layer utilities` directive, ensuring they integrate seamlessly with the framework's purge system. The core animations include `@keyframes reveal-rise` and `@keyframes page-fade` (lines 51-66):

```css
/* From app/globals.css */
@layer utilities {
  .reveal-hidden {
    opacity: 0;
    transform: translateY(20px);
  }
  
  .reveal-play {
    animation: reveal-rise 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  }
  
  .page-enter {
    animation: page-fade 0.5s ease-out;
  }
}

```

These GPU-accelerated transforms use `translateY` and `opacity` exclusively to avoid layout thrashing, ensuring 60fps performance even on lower-end devices.

## Accessibility and Reduced Motion Support

A production-ready animation strategy must respect user accessibility preferences. The implementation includes two layers of protection for users with `prefers-reduced-motion: reduce` settings.

First, the `Reveal` component checks the media query before initializing the observer (lines 29-32):

```tsx
// From components/ds/reveal.tsx
useEffect(() => {
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    setIsVisible(true);
    return;
  }
  // ... observer setup
}, []);

```

Second, the global stylesheet disables animations entirely via CSS (lines 9-17):

```css
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

```

This dual approach ensures immediate content visibility regardless of JavaScript execution timing.

## Practical Implementation Examples

### Basic Content Reveal

Wrap any block-level element to animate it on scroll:

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

export default function ArticleHeader() {
  return (
    <Reveal>
      <h1 className="text-4xl font-bold">The Future of Web Design</h1>
    </Reveal>
  );
}

```

The element applies the `.reveal-hidden` class initially, then receives `.reveal-play` once it enters the viewport, triggering the slide-up effect defined in your CSS utilities.

### Staggered Editorial Lists

For feature lists or table of contents, pass the index as the delay prop:

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

export default function FeatureList() {
  const items = ["Performance", "Accessibility", "Design"];
  return (
    <ul className="space-y-4">
      {items.map((item, i) => (
        <Reveal key={item} delay={i as 0 | 1 | 2 | 3}>
          <li className="text-lg">{item}</li>
        </Reveal>
      ))}
    </ul>
  );
}

```

Each item appears with progressive delays of 0ms, 50ms, 200ms, and 400ms, creating a natural reading flow that guides the eye downward without overwhelming the user.

### Page-Level Entrance Animations

Add the `page-enter` class to your root layout in [`app/layout.tsx`](https://github.com/woosal1337/blog/blob/main/app/layout.tsx) for a global fade-in on initial load:

```tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className="page-enter">
        {children}
      </body>
    </html>
  );
}

```

This applies the `page-fade` keyframe defined in [`globals.css`](https://github.com/woosal1337/blog/blob/main/globals.css), subtly fading the entire page content once the document initializes.

## Summary

- **Use CSS keyframes** defined in [`app/globals.css`](https://github.com/woosal1337/blog/blob/main/app/globals.css) for GPU-accelerated animations that avoid layout thrashing.
- **Leverage IntersectionObserver** via the `Reveal` component in [`components/ds/reveal.tsx`](https://github.com/woosal1337/blog/blob/main/components/ds/reveal.tsx) to trigger animations only when elements become visible.
- **Implement staggered delays** using the component's `delay` prop with values `[0, 50, 200, 400]`ms to create editorial-friendly cascading effects.
- **Respect accessibility** by checking `prefers-reduced-motion: reduce` in both JavaScript and CSS to immediately show content for motion-sensitive users.
- **Avoid animation libraries** to keep bundle size minimal and maintain compatibility with Next.js 14 App Router static optimization.

## Frequently Asked Questions

### What is the best animation strategy for a Next.js editorial site?

The most effective strategy combines CSS-only keyframes with the native IntersectionObserver API, as implemented in the woosal1337/blog repository. This approach keeps JavaScript bundles small, runs animations on the GPU for 60fps performance, and triggers reveals only when content scrolls into view. It avoids the overhead of libraries like Framer Motion or GSAP for simple editorial reveals while maintaining full accessibility support.

### How do you handle staggered animations without JavaScript libraries?

Staggered animations are handled through a delay prop that maps to a static array of millisecond values (`[0, 50, 200, 400]`) in the `Reveal` component. The component applies these values via the `animationDelay` inline style property, which the browser uses to offset the CSS keyframe animation start times. This eliminates the need for JavaScript animation loops or complex state management while still providing smooth, sequential reveals.

### Does this approach support prefers-reduced-motion?

Yes, the implementation includes comprehensive reduced-motion support. The `Reveal` component in [`components/ds/reveal.tsx`](https://github.com/woosal1337/blog/blob/main/components/ds/reveal.tsx) checks `window.matchMedia('(prefers-reduced-motion: reduce)')` before initializing the IntersectionObserver, immediately setting visibility to true if the user prefers minimal motion. Additionally, [`app/globals.css`](https://github.com/woosal1337/blog/blob/main/app/globals.css) contains a media query that forces all animation durations to 0.01ms when this preference is detected, ensuring content appears instantly without motion.

### Where are the animation keyframes defined in this repository?

All keyframe animations are defined in [`app/globals.css`](https://github.com/woosal1337/blog/blob/main/app/globals.css) within the `@layer utilities` block. Specifically, `@keyframes reveal-rise` and `@keyframes page-fade` appear between lines 51-66, while the utility classes `.reveal-hidden`, `.reveal-play`, and `.page-enter` connect these animations to the React components. The reduced-motion media query that overrides these animations is located at lines 9-17 in the same file.