DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Framer Motion Patterns: Variants, Layout Animations, Shared Elements, and Page Transitions

Framer Motion is the most capable React animation library. But most developers use it for simple fade-ins when it's capable of orchestrated sequences, shared layout animations, and gesture-driven interactions that feel native.

Core Concepts

Framer Motion works on a simple model: describe the target state, and the library figures out the transition.

import { motion } from 'framer-motion'

// Basic animation
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: -20 }}
  transition={{ duration: 0.3, ease: 'easeOut' }}
/>

// Viewport-triggered animation
<motion.div
  initial={{ opacity: 0, x: -50 }}
  whileInView={{ opacity: 1, x: 0 }}
  viewport={{ once: true, margin: '-100px' }}
  transition={{ duration: 0.5 }}
/>
Enter fullscreen mode Exit fullscreen mode

Variants for Orchestrated Sequences

Variants let parent components coordinate children:

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,  // each child delays by 100ms
      delayChildren: 0.2,
    },
  },
}

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

function ProductGrid({ products }) {
  return (
    <motion.div
      variants={containerVariants}
      initial='hidden'
      animate='visible'
      className='grid grid-cols-3 gap-6'
    >
      {products.map(product => (
        <motion.div key={product.id} variants={itemVariants}>
          <ProductCard product={product} />
        </motion.div>
      ))}
    </motion.div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Each card fades up one after another — zero per-item configuration needed.

Layout Animations

Animate layout changes automatically:

// List that reorders smoothly
function SortableList({ items }) {
  return (
    <motion.ul layout>
      {items.map(item => (
        <motion.li
          key={item.id}
          layout  // animates position changes
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        >
          {item.name}
        </motion.li>
      ))}
    </motion.ul>
  )
}
Enter fullscreen mode Exit fullscreen mode

Add layout to any motion element and Framer Motion smoothly transitions between positions when items reorder.

Shared Layout with layoutId

Animate elements across different DOM positions:

// Tab indicator that slides between tabs
function Tabs({ tabs, activeTab, onSelect }) {
  return (
    <div className='flex gap-4 border-b'>
      {tabs.map(tab => (
        <button
          key={tab.id}
          onClick={() => onSelect(tab.id)}
          className='relative pb-2'
        >
          {tab.label}
          {activeTab === tab.id && (
            <motion.div
              layoutId='tab-indicator'  // same ID = shared animation
              className='absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500'
            />
          )}
        </button>
      ))}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Gesture Animations

// Draggable card
<motion.div
  drag
  dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
  dragElastic={0.1}
  whileDrag={{ scale: 1.05, rotate: 3 }}
/>

// Button hover + tap
<motion.button
  whileHover={{ scale: 1.02, y: -1 }}
  whileTap={{ scale: 0.98 }}
  transition={{ type: 'spring', stiffness: 400, damping: 17 }}
/>
Enter fullscreen mode Exit fullscreen mode

Page Transitions

// app/layout.tsx -- wrap pages for transitions
import { AnimatePresence } from 'framer-motion'

export default function Layout({ children }) {
  return (
    <AnimatePresence mode='wait'>
      {children}
    </AnimatePresence>
  )
}

// Each page
<motion.main
  initial={{ opacity: 0, y: 8 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: -8 }}
  transition={{ duration: 0.2 }}
>
  {content}
</motion.main>
Enter fullscreen mode Exit fullscreen mode

Performance

Framer Motion uses the Web Animations API and CSS transforms — GPU-accelerated. For performance-critical animations:

  • Animate transform and opacity only (avoids layout)
  • Use will-change: transform on frequently animated elements
  • Use useReducedMotion() to respect user preferences
import { useReducedMotion } from 'framer-motion'

function AnimatedCard({ children }) {
  const prefersReduced = useReducedMotion()
  return (
    <motion.div
      animate={{ y: prefersReduced ? 0 : -4 }}
      whileHover={{ y: prefersReduced ? 0 : -8 }}
    >
      {children}
    </motion.div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The AI SaaS Starter at whoffagents.com ships with Framer Motion configured: page transitions, staggered card grids, and hover animations on CTAs — all with useReducedMotion support. $99 one-time.

Top comments (0)