Skip to main content

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

package.json
{
  "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.
"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

2. Scroll-Triggered Animations

Animate elements when they enter the viewport using whileInView:
src/components/about.tsx
<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:
src/components/about.tsx
<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:
<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:
src/components/hero.tsx
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

src/app/globals.css
@layer utilities {
  .animate-gradient {
    animation: gradient 8s linear infinite;
  }

  @keyframes gradient {
    0% { background-position: 0% 50%; }
    50% { background-position: 100% 50%; }
    100% { background-position: 0% 50%; }
  }
}
src/components/hero.tsx
<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)

src/app/globals.css
@layer utilities {
  .animate-shine {
    animation: shine 1.5s 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

Performance Optimizations

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 */
}

Animate Transform and Opacity

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.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !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:
src/components/hero.tsx
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:
  1. Install Framer Motion DevTools
  2. 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 }} />

Animations Fire on Every Scroll

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

Build docs developers (and LLMs) love