Framer Motion: Animating Component Appearance in React

How to create smooth, reusable appearance animations in React using Framer Motion and AnimatePresence for better user experience.

April 12, 2023

The Animation Challenge

Modern web applications need smooth transitions to feel polished and professional. When components appear and disappear from the DOM, abrupt changes can feel jarring. Users expect subtle animations that guide their attention and provide visual feedback.

The problem? React's conditional rendering happens instantly:

{isVisible && <div>Content appears instantly!</div>}

This works functionally, but there's no transition. The content just pops into existence. We need a way to animate the entrance and exit of conditionally rendered components.

Solution

Framer Motion's AnimatePresence component solves this by tracking components as they're added and removed from the React tree, giving us time to animate their exit before unmounting.

Here's a reusable wrapper component:

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

interface AnimatePresenceProps {
  isVisible: boolean
  children: React.ReactNode | React.ReactNode[]
  animationProps?: React.ComponentProps<typeof motion.div>
}

const AnimateAppearance = ({
  isVisible,
  children,
  animationProps,
}: AnimatePresenceProps) => (
  <AnimatePresence>
    {isVisible && (
      <motion.div
        initial={{ y: 10, opacity: 0 }}
        animate={{ y: 0, opacity: 1 }}
        exit={{ y: -10, opacity: 0 }}
        transition={{ delay: 0.3, duration: 0.3 }}
        {...(animationProps as any)}
      >
        {children}
      </motion.div>
    )}
  </AnimatePresence>
)

export default AnimateAppearance

How It Works

AnimatePresence: Wraps conditionally rendered components and delays their unmounting until exit animations complete.

Motion states:

  • initial - Starting position (slightly below, invisible)
  • animate - Final position (at rest, fully visible)
  • exit - Leaving position (slightly above, invisible)

Transition: Controls timing with 300ms duration and delay, creating a smooth fade-and-slide effect.

Prop spreading: The animationProps parameter lets you override default animations per instance, giving flexibility without sacrificing reusability.

Practical Usage

import AnimateAppearance from '@/components/AnimateAppearance'

const NotificationBanner = () => {
  const [showBanner, setShowBanner] = useState(true)

  return (
    <AnimateAppearance isVisible={showBanner}>
      <div className="banner">
        <p>Your changes have been saved!</p>
        <button onClick={()=> setShowBanner(false)}>Dismiss</button>
      </div>
    </AnimateAppearance>
  )
}

For custom animations per use case:

<AnimateAppearance
  isVisible={showModal}
  animationProps={{
    initial: { scale: 0.9, opacity: 0 },
    animate: { scale: 1, opacity: 1 },
    exit: { scale: 0.9, opacity: 0 },
    transition: { duration: 0.2 },
  }}
>
  <Modal />
</AnimateAppearance>

Why This Matters

Better UX: Smooth animations feel more professional and guide user attention naturally.

Reusability: Write the animation logic once, use it anywhere.

Flexibility: Override defaults when needed while keeping sensible defaults for common cases.

Type safety: Using React.ComponentProps<typeof motion.div> (see TS/React #01: Where are my prop types?!) ensures full autocomplete and type checking for animation properties.

Common Variations

Slide from different directions:

// From right
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -20, opacity: 0 }}

Scale animation:

initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}

Stagger children:

<motion.div
  initial="hidden"
  animate="visible"
  variants={{
    visible: { transition: { staggerChildren: 0.1 } }
  }}
>
  {items.map(item => <AnimateAppearance>{item}</AnimateAppearance>)}
</motion.div>

Performance Considerations

  • Framer Motion uses hardware-accelerated transforms (opacity, x, y, scale) for smooth 60fps animations
  • Avoid animating properties like height, width, or margin as they trigger layout recalculations (but there are uses to it!)
  • Use mode="wait" on AnimatePresence to prevent multiple animations overlapping

Conclusion

The AnimateAppearance component provides a clean abstraction over Framer Motion's animation primitives. By encapsulating the common pattern of appearance/disappearance animations, we can:

  • Add polish to conditional rendering with minimal code
  • Maintain consistency across the application
  • Customize animations when needed without reinventing the wheel
  • Keep components clean and focused on their primary purpose

Next time you're rendering something conditionally, remember: a little motion goes a long way in creating delightful user experiences.