Overview
Animations bring your React applications to life, providing visual feedback and improving user experience. This section covers both CSS-based animations and the popular Framer Motion library.
CSS Animations Built-in browser animations with transitions and keyframes
Framer Motion Powerful animation library for React with declarative API
Exit Animations Animate components when they’re removed from the DOM
Layout Animations Smooth layout changes and element repositioning
CSS Transitions
CSS transitions are the simplest way to add animations to your React components.
Basic Transition
Icon Rotation
Component Usage
.challenge-item-details-icon {
display : inline-block ;
font-size : 0.85 rem ;
margin-left : 0.25 rem ;
transition : transform 0.3 s ease-out ;
}
.challenge-item-details.expanded .challenge-item-details-icon {
transform : rotate ( 180 deg );
}
The transition property defines which CSS properties should animate, the duration, and the timing function (ease-out, ease-in, linear, etc.).
CSS Keyframe Animations
For more complex animations, use CSS @keyframes.
Modal Slide-In Animation
Modal Component
.modal {
top : 10 % ;
border-radius : 6 px ;
padding : 1.5 rem ;
width : 30 rem ;
max-width : 90 % ;
z-index : 10 ;
animation : slide-up-fade-in 0.3 s ease-out forwards ;
}
@keyframes slide-up-fade-in {
0% {
transform : translateY ( 30 px );
opacity : 0 ;
}
100% {
transform : translateY ( 0 );
opacity : 1 ;
}
}
Use forwards in the animation property to maintain the final state after the animation completes.
Common Keyframe Patterns
Fade In
Slide In
Scale & Fade
Bounce
@keyframes fade-in {
from {
opacity : 0 ;
}
to {
opacity : 1 ;
}
}
.element {
animation : fade-in 0.3 s ease-in ;
}
@keyframes slide-in-left {
from {
transform : translateX ( -100 % );
}
to {
transform : translateX ( 0 );
}
}
.element {
animation : slide-in-left 0.4 s ease-out ;
}
@keyframes scale-fade-in {
0% {
transform : scale ( 0.8 );
opacity : 0 ;
}
100% {
transform : scale ( 1 );
opacity : 1 ;
}
}
.element {
animation : scale-fade-in 0.3 s ease-out ;
}
@keyframes bounce {
0% , 100% {
transform : translateY ( 0 );
}
50% {
transform : translateY ( -20 px );
}
}
.element {
animation : bounce 0.6 s ease-in-out infinite ;
}
Framer Motion
Framer Motion is a production-ready animation library for React with a simple, declarative API.
Installation
npm install framer-motion
Basic Animation
import { createPortal } from 'react-dom' ;
import { motion } from 'framer-motion' ;
export default function Modal ({ title , children , onClose }) {
return createPortal (
<>
< div className = "backdrop" onClick = { onClose } />
< motion.dialog
initial = { { opacity: 0 , y: 30 } }
animate = { { opacity: 1 , y: 0 } }
exit = { { opacity: 0 , y: 30 } }
open
className = "modal"
>
< h2 > { title } </ h2 >
{ children }
</ motion.dialog >
</> ,
document . getElementById ( 'modal' )
);
}
Framer Motion uses motion components (like motion.div, motion.dialog) which are enhanced versions of HTML elements with animation capabilities.
Animation Props
The starting state of the animation. < motion.div initial = { { opacity: 0 , scale: 0.5 } } >
The target state to animate to. < motion.div animate = { { opacity: 1 , scale: 1 } } >
The state to animate to when the component is removed. < motion.div exit = { { opacity: 0 , scale: 0.5 } } >
Configure animation timing and easing. < motion.div
transition = { { duration: 0.3 , ease: "easeOut" } }
>
Dynamic Animations
import { motion } from 'framer-motion' ;
export default function ChallengeItem ({ isExpanded , onViewDetails }) {
return (
< div className = "challenge-item-details" >
< button onClick = { onViewDetails } >
View Details { ' ' }
< motion.span
animate = { { rotate: isExpanded ? 180 : 0 } }
className = "challenge-item-details-icon"
>
▲
</ motion.span >
</ button >
</ div >
);
}
Framer Motion automatically handles animations when prop values change. No need to manually trigger transitions!
Exit Animations
Animating component removal requires special handling since React removes elements immediately.
Using AnimatePresence
import { AnimatePresence } from 'framer-motion' ;
import Modal from './Modal' ;
export default function NewChallenge () {
const [ isModalOpen , setIsModalOpen ] = useState ( false );
return (
<>
< button onClick = { () => setIsModalOpen ( true ) } >
New Challenge
</ button >
< AnimatePresence >
{ isModalOpen && (
< Modal
title = "New Challenge"
onClose = { () => setIsModalOpen ( false ) }
>
< form > ... </ form >
</ Modal >
) }
</ AnimatePresence >
</>
);
}
AnimatePresence must be a direct parent of the conditionally rendered component. Each child must have a unique key prop if you’re rendering multiple animating components.
AnimatePresence with Lists
import { AnimatePresence , motion } from 'framer-motion' ;
export default function Challenges ({ challenges }) {
return (
< ul className = "challenge-items" >
< AnimatePresence >
{ challenges . map (( challenge ) => (
< motion.li
key = { challenge . id }
initial = { { opacity: 0 , y: - 20 } }
animate = { { opacity: 1 , y: 0 } }
exit = { { opacity: 0 , y: 20 } }
>
< ChallengeItem challenge = { challenge } />
</ motion.li >
)) }
</ AnimatePresence >
</ ul >
);
}
Animation Variants
Variants let you define reusable animation configurations.
import { motion } from 'framer-motion' ;
const itemVariants = {
hidden: { opacity: 0 , y: 20 },
visible: {
opacity: 1 ,
y: 0 ,
transition: { duration: 0.3 }
},
exit: {
opacity: 0 ,
y: - 20 ,
transition: { duration: 0.2 }
}
};
export default function ChallengeItem ({ challenge }) {
return (
< motion.article
variants = { itemVariants }
initial = "hidden"
animate = "visible"
exit = "exit"
className = "challenge-item"
>
< h2 > { challenge . title } </ h2 >
< p > { challenge . description } </ p >
</ motion.article >
);
}
Variants make your animations more maintainable and enable powerful features like animation orchestration and propagation to children.
Staggered Animations
Create cascading animations where child elements animate in sequence.
import { motion } from 'framer-motion' ;
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1 ,
transition: {
staggerChildren: 0.1
}
}
};
const itemVariants = {
hidden: { opacity: 0 , x: - 20 },
visible: { opacity: 1 , x: 0 }
};
export default function ChallengeList ({ challenges }) {
return (
< motion.ul
variants = { containerVariants }
initial = "hidden"
animate = "visible"
className = "challenge-items"
>
{ challenges . map (( challenge ) => (
< motion.li key = { challenge . id } variants = { itemVariants } >
< ChallengeItem challenge = { challenge } />
</ motion.li >
)) }
</ motion.ul >
);
}
The staggerChildren property automatically delays each child’s animation, creating a smooth cascade effect without manual timing calculations.
Layout Animations
Framer Motion can automatically animate layout changes.
import { motion } from 'framer-motion' ;
export default function ExpandablePanel ({ isExpanded }) {
return (
< motion.div
layout
className = "panel"
>
< h3 > Panel Title </ h3 >
{ isExpanded && (
< motion.div
initial = { { opacity: 0 } }
animate = { { opacity: 1 } }
exit = { { opacity: 0 } }
>
< p > Panel content goes here... </ p >
</ motion.div >
) }
</ motion.div >
);
}
The layout prop tells Framer Motion to animate any layout changes (position, size) automatically using the FLIP technique for optimal performance.
Imperative Animations
Use the useAnimation hook for programmatic control.
import { motion , useAnimation } from 'framer-motion' ;
import { useEffect } from 'react' ;
export default function Notification ({ message , type }) {
const controls = useAnimation ();
useEffect (() => {
controls . start ({
opacity: [ 0 , 1 , 1 , 0 ],
y: [ 20 , 0 , 0 , - 20 ],
transition: {
duration: 3 ,
times: [ 0 , 0.1 , 0.9 , 1 ]
}
});
}, [ message , controls ]);
return (
< motion.div
animate = { controls }
className = { `notification ${ type } ` }
>
{ message }
</ motion.div >
);
}
Gesture Animations
Framer Motion includes built-in gesture recognition.
import { motion } from 'framer-motion' ;
export default function Button ({ children , onClick }) {
return (
< motion.button
onClick = { onClick }
whileHover = { { scale: 1.05 } }
whileTap = { { scale: 0.95 } }
className = "button"
>
{ children }
</ motion.button >
);
}
Animate transform and opacity
Use will-change sparingly
The will-change CSS property can improve performance but use it carefully. .animated-element {
will-change : transform, opacity;
}
Reduce motion for accessibility
Respect user preferences for reduced motion. import { useReducedMotion } from 'framer-motion' ;
function Component () {
const shouldReduceMotion = useReducedMotion ();
return (
< motion.div
animate = { { x: shouldReduceMotion ? 0 : 100 } }
transition = { { duration: shouldReduceMotion ? 0 : 0.3 } }
/>
);
}
Common Animation Patterns
Modal Entry/Exit
Page Transitions
Loading Spinner
Notification Toast
import { AnimatePresence , motion } from 'framer-motion' ;
const modalVariants = {
hidden: { opacity: 0 , scale: 0.8 },
visible: {
opacity: 1 ,
scale: 1 ,
transition: { type: "spring" , damping: 25 }
},
exit: {
opacity: 0 ,
scale: 0.8 ,
transition: { duration: 0.2 }
}
};
function App () {
const [ showModal , setShowModal ] = useState ( false );
return (
< AnimatePresence >
{ showModal && (
< motion.div
variants = { modalVariants }
initial = "hidden"
animate = "visible"
exit = "exit"
>
Modal content
</ motion.div >
) }
</ AnimatePresence >
);
}
import { AnimatePresence , motion } from 'framer-motion' ;
const pageVariants = {
initial: { opacity: 0 , x: - 20 },
animate: { opacity: 1 , x: 0 },
exit: { opacity: 0 , x: 20 }
};
function PageTransition ({ children }) {
return (
< motion.div
variants = { pageVariants }
initial = "initial"
animate = "animate"
exit = "exit"
transition = { { duration: 0.3 } }
>
{ children }
</ motion.div >
);
}
import { motion } from 'framer-motion' ;
export default function Spinner () {
return (
< motion.div
className = "spinner"
animate = { { rotate: 360 } }
transition = { {
duration: 1 ,
repeat: Infinity ,
ease: "linear"
} }
/>
);
}
import { AnimatePresence , motion } from 'framer-motion' ;
export default function Toast ({ message , isVisible }) {
return (
< AnimatePresence >
{ isVisible && (
< motion.div
initial = { { opacity: 0 , y: 50 , scale: 0.3 } }
animate = { { opacity: 1 , y: 0 , scale: 1 } }
exit = { { opacity: 0 , scale: 0.5 , transition: { duration: 0.2 } } }
className = "toast"
>
{ message }
</ motion.div >
) }
</ AnimatePresence >
);
}
Best Practices
Keep animations subtle Animations should enhance UX, not distract. Use durations between 200-400ms for most UI transitions.
Be consistent Use similar timing and easing throughout your app for a cohesive feel.
Consider performance Stick to animating transform and opacity when possible for smooth 60fps animations.
Respect accessibility Always honor the prefers-reduced-motion media query for users with motion sensitivities.
Choosing an Animation Approach
CSS Transitions
Best for: Simple hover effects, state changesPros: Performant, no JavaScript overhead, browser-optimizedCons: Limited control, no exit animations
CSS Keyframes
Best for: Complex multi-step animations, loading indicatorsPros: Full animation control, performant, works without JavaScriptCons: Can’t easily respond to React state changes
Framer Motion
Best for: Dynamic animations, gesture-based interactions, layout animationsPros: Declarative API, powerful features, great DXCons: Adds bundle size, requires JavaScript
Styling Learn about styling approaches for your animated components
Refs & Portals Combine animations with portals for modals and overlays