Inksh logoContactAbout usPrivacy
Author name Hemant Bhatt

The Next.js Motion starter Handbook: Crawlable and animated

NEXT.JS
MOTION

Animations that feel right

Introduction

Here's the thing about animations in Next.js: they're leaf problems. Not root problems. Not branch problems. When you keep interactivity at the edges, the trunk can stay fast, crawlable, and friendly to people on spotty network.

Server Components deliver the scaffolding, Client Components deliver the motion. Separate them cleanly and you get SEO, performance, and a delightful UI—all at once.

The Architecture: Trees and Leaves

Think of your app as a tree. The root? Your layout. The branches? Your page structure, your content sections. The leaves? That button that bounces when you hover. That card that fades in when you scroll.

Root

Layout & data fetching

Server Components keep HTML stable and cacheable.

Branch

Sections & content

Still Server Components. Compose data, render markup, stay SEO friendly.

Leaf

Interactions & motion

Client Components with motion/react bring life to the UI.

Leaf Rule:

Attach motion to the last possible component in the tree. Everything above can—and should—stay on the server.

Installing Motion

First things first:

Terminal
npm install motion

Using Motion in Next.js

Motion only works in Client Components

Here's the only rule you need to remember: Motion only works in Client Components.

Why? Because motion needs the browser. It needs event listeners, it needs animation frames, it needs the DOM. The server has none of these.

So whenever you want to use motion, you mark your component with "use client":

components/AnimatedButton.tsx
1"use client";
2
3import { motion } from "motion/react";
4
5export function AnimatedButton() {
6 return (
7 <motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
8 Click me
9 </motion.button>
10 );
11}

That "use client" directive tells Next.js: "This component needs the browser. Ship it to the client."

And now you can import that animated button anywhere—even in Server Components:

app/page.tsx
1// app/page.tsx (Server Component)
2import { AnimatedButton } from "./animated-button";
3
4export default function Page() {
5 return (
6 <section>
7 <h1>Welcome</h1>
8 <AnimatedButton /> {/* Client Component leaf */}
9 </section>
10 );
11}

The rest of the page? Pure HTML from the server. Fast. Crawlable. SEO-friendly.

The button? Interactive. Animated. Delightful.

The Basics: Meet <motion />

Every HTML/SVG tag has a motion twin

Motion gives you an alternate version of every HTML and SVG element that you would like animated. Instead of <div>, you write <motion.div>. Instead of <button>, you write <motion.button>. These components work exactly like their regular counterparts—you can pass the same props, the same className, the same children. But now they have superpowers.

Motion twins
1<motion.div />
2<motion.button />
3<motion.section />
4<motion.svg />
5<motion.circle />

Enter animations

When an element enters the DOM, animate it:

Enter animation
1<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}>
2 I fade in and slide up when I appear
3</motion.div>

initial defines where the animation starts. animate defines where it ends. Motion handles the in-between.

SSR-Ready Animations

Motion components work seamlessly with server-side rendering. When you set initial={false}, the server will render the final animated state directly in the HTML—no flicker, no layout shift. When the inital value is set, the initial value is rendered in the server.

SSR with final state
1// Server renders rotate(45deg) immediately
2<motion.div initial={false} animate={{ rotate: 45 }} />
SSR with initial state
1// Server will output rotate(50deg)
2<motion.div initial={{ rotate: 50 }} animate={{ rotate: 90 }} />

Perfect for critical UI elements that need to appear in their final state instantly, while still maintaining animation capabilities on the client.

What Is "In-Between"?

Here's the magic: you tell Motion where to start and where to end, and it figures out everything in between.

Tweening
1<motion.div initial={{ x: 0 }} animate={{ x: 100 }} />

You defined two states: x: 0 and x: 100. But what about x: 25? x: 50? x: 73.8?

Motion calculates all of those intermediate values for you. This process is called interpolation, or in animation terms, in-betweening (or "tweening").

Traditional animation required artists to draw every frame between keyframes by hand. Motion does this automatically, mathematically.

But it's not just linear movement. Motion applies physics and easing curves to make animations feel natural:

Spring
1// Spring physics - bouncy, organic
2<motion.div
3 animate={{ x: 100 }}
4 transition={{ type: "spring", stiffness: 300 }}
5/>
Ease
1// Easing curve - smooth, controlled
2<motion.div
3 animate={{ x: 100 }}
4 transition={{ duration: 0.5, ease: "easeInOut" }}
5/>

With spring physics, Motion calculates each in-between frame based on velocity, stiffness, and damping—like a real physical spring. With easing curves, it applies mathematical functions (quadratic, cubic, etc.) to control acceleration and deceleration.

Gesture animations

Want something to happen on hover? On tap? On drag?

Gesture
1<motion.button
2 whileHover={{ scale: 1.1 }}
3 whileTap={{ scale: 0.9 }}
4 whileDrag={{ rotate: 10 }}
5 drag
6>
7 Hover me, tap me, drag me
8</motion.button>

No event listeners. No state management. Just declarative animation props.

Scroll-triggered animations

This is where it gets fun. Animate elements as they scroll into view:

Scroll
1<motion.article
2 initial={{ opacity: 0, scale: 0.8 }}
3 whileInView={{ opacity: 1, scale: 1 }}
4 viewport={{ once: true }}
5>
6 I animate when you scroll to me
7</motion.article>

Perfect for blog cards, feature sections, anything that should reveal itself as the user scrolls.

Exit animations

Elements leaving the DOM can animate out too. Wrap them in AnimatePresence:

AnimatePresence
1import { AnimatePresence, motion } from "motion/react";
2
3<AnimatePresence>
4 {isVisible && (
5 <motion.div
6 initial={{ opacity: 0 }}
7 animate={{ opacity: 1 }}
8 exit={{ opacity: 0 }}
9 >
10 I fade out when I leave
11 </motion.div>
12 )}
13</AnimatePresence>;

What Motion Can Animate

Pretty much every visual property is fair game. Mix and match transforms, CSS props, layout, and SVG attributes.

Here's the beautiful part: you can animate each transform property independently.

Combined transforms
1<motion.div
2 animate={{ x: 100 }}
3 whileHover={{ scale: 1.2 }}
4 whileTap={{ rotate: 45 }}
5/>

Transitions: Timing the In-Between

Remember the "In-Between" we talked about earlier? Here's a secret: there are only two ways to make things move 'in-between'.

Duration-based: "Move this 100 pixels to the left in exactly 0.5 seconds." Like a video editor. Precise. Choreographed. You say when it starts, you say when it stops.

Physics-based: "Make this feel like a spring with this much stiffness." Like the real world. Bouncy. Alive. It settles when it's ready, not when you tell it to.

Duration comes from traditional animation—animators counting frames, video editors scrubbing timelines. Physics comes from trying to make computers feel less like computers.

Both are useful.

By default, Motion picks sensible transitions. Physical properties like x and scale animate with spring physics. Visual properties like opacity animate with easing curves. But you can override:

Physics-Based

Feels organic. Settles when the spring is done.

Spring control
1<motion.div
2 animate={{ x: 100 }}
3 transition={{ type: "spring", stiffness: 300, damping: 20 }}
4/>

Duration-Based

Feels choreographed. Starts and stops exactly when you say.

Duration control
1<motion.div
2 animate={{ opacity: 1 }}
3 transition={{ duration: 0.5, ease: "easeOut" }}
4/>

Mix transitions per property

Give positional transforms springs while visual changes use durations.

Hybrid transitions
1<motion.div
2 animate={{ x: 100, opacity: 1 }}
3 transition={{
4 x: { type: "spring" },
5 opacity: { duration: 0.3 }
6 }}
7/>

Quick decision tree

Need instant feedback on hover?whileHover.

Want scroll storytelling?whileInViewwithviewport={{ once: true }}.

Removing elements? Wrap them inAnimatePresence.

Conclusion

Motion in Next.js isn't complicated. It's just about knowing where the boundary is.

Server Components for structure. Client Components for interaction. Motion at the leaves.

Install it, mark your components with "use client", and start animating. Use initial and animate for enter animations. Use whileHover and whileTap for gestures. Use whileInView for scroll-triggered reveals.

And remember: animation is a leaf problem. Keep it at the edges, and your app will be fast, crawlable, and still full of life.

Need more recipes? The motion docs are waiting at motion.dev/docs/react-animation.