Skip to main content
{/}codefunc
GuidesJuly 7, 2026 · 7 min read

Web Animations: Framer Motion and CSS for React Developers

A practical guide to animations in React — CSS transitions and keyframes, Framer Motion for complex interactions, performance optimization, and accessibility considerations.

By codefunc

animationframer-motioncssreactfrontendux

Animations make interfaces feel responsive and alive. A well-timed fade, a smooth transition, or a satisfying bounce provides feedback and guides attention. But animations done wrong — too slow, too distracting, or inaccessible — hurt more than they help. Understanding when to use CSS animations versus JavaScript libraries like Framer Motion is essential.

Bottom line: Use CSS transitions for simple state changes (hover, focus). Use CSS keyframes for looping or complex multi-step animations. Use Framer Motion for gesture-based interactions, layout animations, and coordinated sequences. Always respect prefers-reduced-motion. Animate transform and opacity for best performance — avoid animating width, height, or top/left.

CSS transitions

Transitions animate changes between states.

Basic syntax

.button {
  background-color: #3b82f6;
  transition: background-color 200ms ease;
}

.button:hover {
  background-color: #2563eb;
}

Multiple properties

.card {
  transform: scale(1);
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  transition: 
    transform 200ms ease,
    box-shadow 200ms ease;
}

.card:hover {
  transform: scale(1.02);
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

Transition shorthand

/* property | duration | timing-function | delay */
transition: transform 300ms ease-out 100ms;

/* All properties */
transition: all 200ms ease;

Timing functions

Function Description
linear Constant speed
ease Slow start, fast middle, slow end
ease-in Slow start
ease-out Slow end
ease-in-out Slow start and end
cubic-bezier() Custom curve
/* Custom easing */
transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);

CSS keyframes

Keyframes define multi-step animations.

Basic animation

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.element {
  animation: fadeIn 300ms ease forwards;
}

Multi-step animation

@keyframes bounce {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-20px);
  }
}

.bouncing {
  animation: bounce 600ms ease-in-out infinite;
}

Animation shorthand

/* name | duration | timing | delay | iterations | direction | fill-mode */
animation: slideIn 500ms ease-out 100ms 1 normal forwards;

Useful animations

/* Fade in up */
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Pulse */
@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

/* Spin */
@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

/* Shake */
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-5px); }
  75% { transform: translateX(5px); }
}

CSS in React

Conditional classes

function Button({ isLoading }) {
  return (
    <button className={`btn ${isLoading ? 'btn-loading' : ''}`}>
      {isLoading ? 'Loading...' : 'Submit'}
    </button>
  );
}
.btn {
  transition: opacity 200ms ease;
}

.btn-loading {
  opacity: 0.7;
  pointer-events: none;
}

CSS Modules with animations

/* Button.module.css */
.button {
  transition: transform 200ms ease;
}

.button:active {
  transform: scale(0.95);
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.fadeIn {
  animation: fadeIn 300ms ease;
}

Framer Motion

Framer Motion provides declarative animations for React.

Setup

npm install framer-motion

Basic animation

import { motion } from 'framer-motion';

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

Animate on mount

function Card() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.4, ease: 'easeOut' }}
      className="card"
    >
      Card content
    </motion.div>
  );
}

Hover and tap

function Button({ children }) {
  return (
    <motion.button
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      transition={{ type: 'spring', stiffness: 400, damping: 17 }}
    >
      {children}
    </motion.button>
  );
}

Exit animations (AnimatePresence)

import { motion, AnimatePresence } from 'framer-motion';

function Modal({ isOpen, onClose, children }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="modal-backdrop"
          onClick={onClose}
        >
          <motion.div
            initial={{ opacity: 0, scale: 0.9 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.9 }}
            transition={{ type: 'spring', damping: 25 }}
            className="modal"
            onClick={(e) => e.stopPropagation()}
          >
            {children}
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Variants

Define animation states for reuse and orchestration:

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 },
};

function List({ items }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {items.map((item) => (
        <motion.li key={item.id} variants={itemVariants}>
          {item.name}
        </motion.li>
      ))}
    </motion.ul>
  );
}

Layout animations

Animate layout changes automatically:

function ExpandableCard({ isExpanded, onClick }) {
  return (
    <motion.div
      layout
      onClick={onClick}
      style={{ borderRadius: 12 }}
    >
      <motion.h2 layout="position">Title</motion.h2>
      <AnimatePresence>
        {isExpanded && (
          <motion.p
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            Expanded content here...
          </motion.p>
        )}
      </AnimatePresence>
    </motion.div>
  );
}

Shared layout animations

Animate between different components:

function Tabs({ tabs, activeTab, setActiveTab }) {
  return (
    <div className="tabs">
      {tabs.map((tab) => (
        <button
          key={tab.id}
          onClick={() => setActiveTab(tab.id)}
          className={activeTab === tab.id ? 'active' : ''}
        >
          {tab.label}
          {activeTab === tab.id && (
            <motion.div
              layoutId="activeTab"
              className="underline"
              transition={{ type: 'spring', stiffness: 500, damping: 30 }}
            />
          )}
        </button>
      ))}
    </div>
  );
}

Gestures

function DraggableCard() {
  return (
    <motion.div
      drag
      dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
      dragElastic={0.2}
      whileDrag={{ scale: 1.1 }}
    >
      Drag me
    </motion.div>
  );
}

Scroll animations

import { motion, useScroll, useTransform } from 'framer-motion';

function ParallaxHero() {
  const { scrollY } = useScroll();
  const y = useTransform(scrollY, [0, 500], [0, 150]);
  const opacity = useTransform(scrollY, [0, 300], [1, 0]);

  return (
    <motion.div style={{ y, opacity }} className="hero">
      <h1>Parallax Hero</h1>
    </motion.div>
  );
}

Animate in view

import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';

function Section({ children }) {
  const ref = useRef(null);
  const isInView = useInView(ref, { once: true, margin: '-100px' });

  return (
    <motion.section
      ref={ref}
      initial={{ opacity: 0, y: 50 }}
      animate={isInView ? { opacity: 1, y: 0 } : {}}
      transition={{ duration: 0.6 }}
    >
      {children}
    </motion.section>
  );
}

Performance

Animate the right properties

Good (GPU accelerated) Bad (triggers layout)
transform width, height
opacity top, left, right, bottom
filter margin, padding
/* Bad - triggers layout */
.element {
  transition: width 300ms;
}

/* Good - GPU accelerated */
.element {
  transition: transform 300ms;
  transform: scaleX(1);
}
.element.expanded {
  transform: scaleX(1.5);
}

Use will-change sparingly

/* Only use when animation is imminent */
.element:hover {
  will-change: transform;
}

/* Don't apply to everything */
* {
  will-change: transform; /* Bad! */
}

Reduce motion for accessibility

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
// Framer Motion respects this automatically, or use hook
import { useReducedMotion } from 'framer-motion';

function AnimatedComponent() {
  const shouldReduceMotion = useReducedMotion();
  
  return (
    <motion.div
      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
    >
      Content
    </motion.div>
  );
}

When to use what

Use case Solution
Hover/focus states CSS transitions
Loading spinners CSS keyframes
Page transitions Framer Motion
List reordering Framer Motion layout
Scroll-linked effects Framer Motion useScroll
Drag and drop Framer Motion gestures
Complex sequences Framer Motion variants
Simple fades CSS transitions or Framer Motion

Tools

Use CSS Minifier to optimize animation stylesheets. Generate border radius values with Border Radius Generator. Convert colors for animations with Color Converter.

Practical takeaway

Start with CSS transitions for simple state changes — they're performant and require no JavaScript. Use CSS keyframes for loading states and continuous animations. Reach for Framer Motion when you need exit animations, gesture interactions, or layout animations. Always animate transform and opacity for smoothness. Respect reduced motion preferences. Keep animations brief (200-500ms) and purposeful — animation should enhance UX, not distract from it.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool