Skip to main content
Back to Blog
Tutorials
2 min read
November 30, 2024

How to Add Animations to Your Website with Framer Motion

Add smooth, performant animations to your React website using Framer Motion. Covers entrance animations, scroll-triggered effects, page transitions, and gesture interactions.

Ryel Banfield

Founder & Lead Developer

Animations make websites feel alive and professional. Framer Motion provides a declarative API for creating animations in React. Here are the most useful patterns.

Installation

pnpm add motion

Basic Entrance Animation

"use client";

import { motion } from "motion/react";

export function FadeIn({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      {children}
    </motion.div>
  );
}
  • initial: Starting state (invisible, shifted down 20px)
  • animate: End state (visible, original position)
  • transition: How the animation plays (0.5 seconds)

Scroll-Triggered Animations

Animate elements when they enter the viewport:

"use client";

import { motion } from "motion/react";

export function ScrollReveal({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 40 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: "-100px" }}
      transition={{ duration: 0.6, ease: "easeOut" }}
    >
      {children}
    </motion.div>
  );
}
  • whileInView: Triggers when element enters viewport
  • viewport.once: Only animates once (does not replay on scroll back)
  • viewport.margin: Triggers 100px before entering viewport

Staggered Children

Animate a list of items one after another:

"use client";

import { motion } from "motion/react";

const container = {
  hidden: { opacity: 0 },
  show: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
    },
  },
};

const item = {
  hidden: { opacity: 0, y: 20 },
  show: { opacity: 1, y: 0 },
};

export function StaggeredList({ items }: { items: string[] }) {
  return (
    <motion.ul variants={container} initial="hidden" animate="show">
      {items.map((text, i) => (
        <motion.li key={i} variants={item}>
          {text}
        </motion.li>
      ))}
    </motion.ul>
  );
}

Each child animates 0.1 seconds after the previous one.

Hover and Tap Animations

<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
  className="rounded-md bg-blue-600 px-6 py-3 text-white"
>
  Click Me
</motion.button>

Subtle scale changes give buttons a tactile feel.

Page Transitions

Wrap your page content with AnimatePresence:

// app/(site)/template.tsx
"use client";

import { motion } from "motion/react";

export default function Template({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
}

Using template.tsx in Next.js App Router re-renders on every navigation, triggering the entrance animation.

Animated Counter

"use client";

import { useInView, useMotionValue, useTransform, animate } from "motion/react";
import { useEffect, useRef } from "react";

export function AnimatedCounter({ target, duration = 2 }: { target: number; duration?: number }) {
  const ref = useRef<HTMLSpanElement>(null);
  const isInView = useInView(ref, { once: true });
  const count = useMotionValue(0);

  useEffect(() => {
    if (isInView) {
      animate(count, target, { duration });
    }
  }, [isInView, count, target, duration]);

  useEffect(() => {
    const unsubscribe = count.on("change", (latest) => {
      if (ref.current) {
        ref.current.textContent = Math.round(latest).toLocaleString();
      }
    });
    return unsubscribe;
  }, [count]);

  return <span ref={ref}>0</span>;
}

Usage:

<p>
  <AnimatedCounter target={500} /> projects delivered
</p>

Accordion Animation

"use client";

import { motion, AnimatePresence } from "motion/react";
import { useState } from "react";

export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="border-b">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex w-full items-center justify-between py-4 text-left font-medium"
      >
        {title}
        <motion.span
          animate={{ rotate: isOpen ? 180 : 0 }}
          transition={{ duration: 0.2 }}
        >
          β–Ό
        </motion.span>
      </button>
      <AnimatePresence>
        {isOpen && (
          <motion.div
            initial={{ height: 0, opacity: 0 }}
            animate={{ height: "auto", opacity: 1 }}
            exit={{ height: 0, opacity: 0 }}
            transition={{ duration: 0.3 }}
            className="overflow-hidden"
          >
            <div className="pb-4">{children}</div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

Performance Tips

  1. Animate transform and opacity only: These properties are GPU-accelerated
  2. Use layout prop sparingly: Layout animations can be expensive
  3. Set once: true on scroll animations: Prevents re-animation
  4. Avoid animating height directly: Use scaleY instead when possible
  5. Use will-change cautiously: Framer Motion handles this internally

Reduced Motion Support

Respect user preferences:

import { useReducedMotion } from "motion/react";

export function AnimatedCard({ children }: { children: React.ReactNode }) {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      initial={shouldReduceMotion ? false : { opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.5 }}
    >
      {children}
    </motion.div>
  );
}

When the user has "Reduce Motion" enabled in their OS settings, animations are disabled.

Common Mistake: Client Component Boundary

Framer Motion components must be client components. Mark them with "use client". Import them into Server Components without issues:

// Server Component (no "use client")
import { FadeIn } from "@/components/FadeIn"; // Client component

export default function Page() {
  return (
    <FadeIn>
      <h1>This works fine</h1>
    </FadeIn>
  );
}

Need Animated Design?

We create engaging, animated websites that perform well across all devices. Contact us for professional web design with tasteful animations.

Framer MotionanimationReactUItutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles