Skip to content
Back to writing
Next.jsPerformanceSample

Scroll-linked animation in the Next.js App Router

June 18, 20264 min readBy Somesh Kumar Mishra

Motion that survives the App Router

Server components cannot hold animation state, so every motion primitive has to sit behind a client boundary. The trick is putting that boundary as low in the tree as possible — wrap the animated element, not the page.

Keep the boundary small

import { motion } from 'framer-motion';

export function FadeIn({ children }) { return ( <motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: '-40px' }} transition={{ duration: 0.5 }} > {children} </motion.div> ); } ```

The page stays a server component; only the wrapper ships JavaScript.

Animate the cheap properties

Stick to `transform` and `opacity`. Both are handled on the compositor, so they never trigger layout or paint. Animating `width`, `height`, `top` or `left` forces a reflow on every frame and will drop frames on mid-range phones.

Respect reduced motion

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

One rule in your global stylesheet covers every animation on the site.