Overview
Thalyson’s Portfolio uses Framer Motion (v12.17.0) to create smooth, performant animations throughout the interface. The animation system includes:
Page entrance animations with stagger effects
Scroll-triggered animations via whileInView
Hover and interaction animations for enhanced UX
Spring physics for natural motion
Performance optimizations with will-change and reduced motion support
All animated components are client components ("use client") since Framer Motion requires browser APIs.
Installation
{
"dependencies" : {
"framer-motion" : "^12.17.0"
}
}
Import in components:
import { motion } from "framer-motion" ;
Core Animation Patterns
1. Container + Item Stagger
The most common pattern: parent container staggers child animations.
Hero Component (src/components/hero.tsx)
About Component (src/components/about.tsx)
"use client" ;
import { motion } from "framer-motion" ;
const ANIMATION_CONFIG = {
container: {
hidden: { opacity: 0 },
visible: {
opacity: 1 ,
transition: {
staggerChildren: 0.2 , // 200ms delay between children
delayChildren: 0.1 // Start after 100ms
}
}
},
item: {
hidden: { opacity: 0 , y: 50 },
visible: {
opacity: 1 ,
y: 0 ,
transition: {
type: "spring" ,
damping: 20 , // Less bouncy
stiffness: 100 , // Spring speed
duration: 0.8
}
}
}
};
export function Hero () {
return (
< motion.div
variants = { ANIMATION_CONFIG . container }
initial = "hidden"
animate = "visible"
>
< motion.h1 variants = { ANIMATION_CONFIG . item } >
Title
</ motion.h1 >
< motion.p variants = { ANIMATION_CONFIG . item } >
Description
</ motion.p >
< motion.button variants = { ANIMATION_CONFIG . item } >
CTA
</ motion.button >
</ motion.div >
);
}
Key properties:
staggerChildren: Delay between each child animation (in seconds)
delayChildren: Initial delay before children start animating
variants: Reusable animation states shared between parent and children
Animate elements when they enter the viewport using whileInView:
< motion.div
initial = "hidden"
whileInView = "visible"
viewport = { {
once: true , // Animate only once (don't re-trigger)
margin: "-100px" // Trigger 100px before element enters viewport
} }
variants = { fadeUp }
>
Content animates when scrolled into view
</ motion.div >
Viewport options:
once: true - Prevents re-animation on every scroll
margin: "-100px" - Adjusts trigger point (negative = earlier, positive = later)
amount: 0.5 - Trigger when 50% of element is visible
3. Hover Animations
Interactive hover effects for cards and buttons:
< motion.div
variants = { fadeUp }
whileHover = { {
y: - 4 , // Lift 4px
scale: 1.02 // Slight scale increase
} }
className = "bg-card p-6 cursor-pointer"
>
Hover me!
</ motion.div >
Common hover patterns:
Lift Effect
Scale Effect
Rotate Effect
< motion.div whileHover = { { y: - 8 , scale: 1.02 } } >
Card
</ motion.div >
4. Spring Physics
Spring animations create natural, physics-based motion:
const springConfig = {
type: "spring" ,
damping: 20 , // Higher = less bouncy (0-100)
stiffness: 100 , // Higher = faster (0-500)
mass: 1 // Higher = heavier, slower (default: 1)
};
< motion.div
initial = { { scale: 0 } }
animate = { { scale: 1 } }
transition = { springConfig }
>
Bouncy element
</ motion.div >
Spring parameters:
damping : Controls bounciness (20-30 for subtle, 10 for bouncy)
stiffness : Controls speed (100 for smooth, 300 for snappy)
mass : Controls inertia (rarely needs adjustment)
5. Title Scale Animation
Special effect for hero titles:
const title = {
hidden: { opacity: 0 , scale: 0.8 },
visible: {
opacity: 1 ,
scale: 1 ,
transition: {
type: "spring" ,
damping: 15 ,
stiffness: 100 ,
duration: 1
}
}
};
< motion.h1 variants = { title } >
Scaling Title
</ motion.h1 >
Animation Recipes
Fade Up (Most Common)
const fadeUp = {
hidden: { opacity: 0 , y: 20 },
visible: {
opacity: 1 ,
y: 0 ,
transition: { duration: 0.5 , ease: "easeOut" }
}
};
Use for: Text blocks, cards, sections
Fade In (Simple)
const fadeIn = {
hidden: { opacity: 0 },
visible: {
opacity: 1 ,
transition: { duration: 0.4 }
}
};
Use for: Images, overlays, backgrounds
Scale In
const scaleIn = {
hidden: { opacity: 0 , scale: 0.95 },
visible: {
opacity: 1 ,
scale: 1 ,
transition: { duration: 0.5 , ease: "easeOut" }
}
};
Use for: Modal dialogs, profile images, featured content
Slide In (Horizontal)
const slideInLeft = {
hidden: { opacity: 0 , x: - 50 },
visible: {
opacity: 1 ,
x: 0 ,
transition: { type: "spring" , damping: 20 , stiffness: 100 }
}
};
Use for: Sidebar navigation, slide-out panels
Stagger List Items
const list = {
visible: { transition: { staggerChildren: 0.1 } }
};
const item = {
hidden: { opacity: 0 , y: 10 },
visible: { opacity: 1 , y: 0 }
};
< motion.ul variants = { list } initial = "hidden" animate = "visible" >
{ items . map ( item => (
< motion.li key = { item . id } variants = { item } >
{ item . text }
</ motion.li >
)) }
</ motion.ul >
CSS Animations (Non-Framer)
Some animations use pure CSS for better performance:
Gradient Animation
@layer utilities {
.animate-gradient {
animation : gradient 8 s linear infinite ;
}
@keyframes gradient {
0% { background-position : 0 % 50 % ; }
50% { background-position : 100 % 50 % ; }
100% { background-position : 0 % 50 % ; }
}
}
< h1 className = "bg-gradient-to-r from-white via-primary to-white bg-[length:200%_auto] bg-clip-text text-transparent animate-gradient" >
Animated Gradient Text
</ h1 >
Shine Effect (Hover)
@layer utilities {
.animate-shine {
animation : shine 1.5 s ease-in-out infinite ;
}
@keyframes shine {
from { transform : translateX ( -200 % ); }
to { transform : translateX ( 200 % ); }
}
}
< button className = "relative group overflow-hidden" >
< div className = "absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-[200%] group-hover:animate-shine pointer-events-none" />
< span > Hover for Shine </ span >
</ button >
Animated Number Counter
src/components/animated-number.tsx
"use client" ;
import { useEffect , useState } from "react" ;
export function AnimatedNumber ({ value , simbol = "+" }) {
const [ count , setCount ] = useState ( 0 );
useEffect (() => {
let start = 0 ;
const end = value ;
const duration = 2000 ; // 2 seconds
const increment = end / ( duration / 16 ); // 60fps
const timer = setInterval (() => {
start += increment ;
if ( start >= end ) {
setCount ( end );
clearInterval ( timer );
} else {
setCount ( Math . floor ( start ));
}
}, 16 );
return () => clearInterval ( timer );
}, [ value ]);
return < span className = "text-2xl font-bold" > { count }{ simbol } </ span > ;
}
Usage:
< AnimatedNumber value = { 5000 } simbol = "+" />
SVG Path Animations
The animated background uses SVG animateMotion:
src/components/animatedBackground.tsx
< svg viewBox = "0 0 602 602" >
< ellipse cx = "295" cy = "193" rx = "1.07" ry = "1.07" fill = "#ef4444" >
< animateMotion dur = "10s" repeatCount = "indefinite" rotate = "auto" >
< mpath xlinkHref = "#path_2" />
</ animateMotion >
</ ellipse >
< path d = "M294.685 193.474L268.932 219.258" stroke = "url(#paint3_linear)" >
< animateMotion dur = "10s" repeatCount = "indefinite" rotate = "auto" >
< mpath xlinkHref = "#path_2" />
</ animateMotion >
</ path >
</ svg >
Key attributes:
dur: Animation duration
repeatCount="indefinite": Loops forever
rotate="auto": Element rotates to follow path
<mpath xlinkHref="#path_2" />: References path to follow
Use will-change Sparingly
Framer Motion automatically applies will-change for animated properties. Avoid manually adding it:
/* Avoid overuse */
.element {
will-change : transform, opacity; /* Can hurt performance */
}
These properties are GPU-accelerated and performant:
// ✅ Performant
< motion.div animate = { { x: 100 , opacity: 0.5 } } />
// ❌ Avoid (causes reflows)
< motion.div animate = { { width: 300 , height: 200 } } />
Use layout Prop for Layout Animations
When animating size or position changes:
< motion.div layout >
Content that changes size smoothly
</ motion.div >
Framer Motion uses FLIP technique for performant layout animations.
Reduce Motion for Accessibility
Respect user preferences for reduced motion:
import { useReducedMotion } from "framer-motion" ;
function Component () {
const shouldReduceMotion = useReducedMotion ();
return (
< motion.div
initial = { { opacity: 0 , y: shouldReduceMotion ? 0 : 50 } }
animate = { { opacity: 1 , y: 0 } }
/>
);
}
Or use CSS:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration : 0.01 ms !important ;
animation-iteration-count : 1 !important ;
transition-duration : 0.01 ms !important ;
}
}
Lazy Load Animations
For off-screen content, use whileInView with once: true:
< motion.div
initial = "hidden"
whileInView = "visible"
viewport = { { once: true } } // Only animate on first view
>
Content
</ motion.div >
Timing Functions (Easing)
Framer Motion supports multiple easing functions:
// Linear (constant speed)
transition = {{ ease : "linear" , duration : 1 }}
// Ease Out (fast start, slow end) - Best for entrances
transition = {{ ease : "easeOut" , duration : 0.5 }}
// Ease In (slow start, fast end) - Best for exits
transition = {{ ease : "easeIn" , duration : 0.3 }}
// Ease In-Out (slow start and end) - General purpose
transition = {{ ease : "easeInOut" , duration : 0.6 }}
// Custom cubic-bezier
transition = {{ ease : [ 0.43 , 0.13 , 0.23 , 0.96 ], duration : 0.8 }}
Recommended defaults:
Entrances: easeOut with 0.5s duration
Exits: easeIn with 0.3s duration
Interactions: easeInOut with 0.2s duration
Custom Animation Hooks
useInView Hook
Track when an element enters the viewport:
import { useInView } from "framer-motion" ;
import { useRef } from "react" ;
function Component () {
const ref = useRef ( null );
const isInView = useInView ( ref , { once: true , margin: "-100px" });
return (
< div ref = { ref } style = { { opacity: isInView ? 1 : 0 } } >
Content
</ div >
);
}
useAnimation Hook
Programmatic control over animations:
import { motion , useAnimation } from "framer-motion" ;
import { useEffect } from "react" ;
function Component () {
const controls = useAnimation ();
useEffect (() => {
controls . start ({
opacity: 1 ,
y: 0 ,
transition: { duration: 0.5 }
});
}, [ controls ]);
return (
< motion.div initial = { { opacity: 0 , y: 20 } } animate = { controls } >
Content
</ motion.div >
);
}
Animation Best Practices
Don’t animate on every render. Use initial and animate props instead of inline styles that change on state updates.
DO ✅
// Efficient: Framer Motion handles optimization
< motion.div
initial = { { opacity: 0 } }
animate = { { opacity: 1 } }
>
Content
</ motion.div >
DON’T ❌
// Inefficient: Triggers animation on every render
< motion.div style = { { opacity: isVisible ? 1 : 0 } } >
Content
</ motion.div >
Keep Animations Subtle
Duration: 0.3-0.6s for most interactions
Distance: Move 10-30px for fade-ups
Scale: 1.02-1.05 for hover effects
Stagger: 0.05-0.2s between items
Too aggressive:
// ❌ Over-the-top
< motion.div whileHover = { { scale: 1.5 , rotate: 360 } } >
Subtle and professional:
// ✅ Subtle enhancement
< motion.div whileHover = { { scale: 1.02 , y: - 4 } } >
Use Consistent Timing
Define animation constants at the top of files:
const ANIMATION_CONFIG = {
container: {
hidden: { opacity: 0 },
visible: {
opacity: 1 ,
transition: { staggerChildren: 0.2 , delayChildren: 0.1 }
}
},
item: {
hidden: { opacity: 0 , y: 50 },
visible: {
opacity: 1 ,
y: 0 ,
transition: { type: "spring" , damping: 20 , stiffness: 100 }
}
}
} as const ;
Benefits:
Easy to adjust all animations at once
TypeScript autocomplete for variant names
Reusable across components
Avoid Animating Width/Height
Animating dimensions causes layout recalculation:
// ❌ Causes reflow
< motion.div animate = { { width: 300 } } />
// ✅ Use scale instead
< motion.div animate = { { scaleX: 1.5 } } />
Optimize for Mobile
Mobile devices have less GPU power. Reduce complexity:
import { useMediaQuery } from "@/hooks/useMediaQuery" ;
function Component () {
const isMobile = useMediaQuery ( "(max-width: 768px)" );
return (
< motion.div
animate = { { y: isMobile ? 0 : 100 } } // Disable on mobile
transition = { { duration: isMobile ? 0 : 0.5 } }
>
Content
</ motion.div >
);
}
Debugging Animations
Visualize Animation State
Use React DevTools with Framer Motion:
Install Framer Motion DevTools
Add to app:
import { MotionDevTools } from "framer-motion-devtools" ;
function App () {
return (
<>
< MotionDevTools />
< Component />
</>
);
}
Log Animation Events
< motion.div
animate = { { opacity: 1 } }
onAnimationStart = { () => console . log ( "Animation started" ) }
onAnimationComplete = { () => console . log ( "Animation completed" ) }
>
Content
</ motion.div >
Common Issues
Animations Not Running
Cause: Using Framer Motion in Server Components
Fix: Add "use client" directive:
"use client" ;
import { motion } from "framer-motion" ;
Janky Animations
Cause: Animating expensive properties (width, height, top, left)
Fix: Use transform properties (x, y, scale):
// ❌ Janky
< motion.div animate = { { left: 100 } } />
// ✅ Smooth
< motion.div animate = { { x: 100 } } />
Cause: Missing viewport={{ once: true }}
Fix:
< motion.div
whileInView = "visible"
viewport = { { once: true } } // Add this
>
Next Steps
Styling System Customize colors and design tokens
Content Management Update portfolio content