Skip to main content

Overview

This portfolio uses Framer Motion for declarative, performant animations combined with Tailwind CSS for utility-based animations. The approach focuses on enhancing user experience without sacrificing performance.
Framer Motion version: ^12.34.5

Framer Motion Integration

Framer Motion is imported and used throughout the application:
import { motion } from "framer-motion"

Why Framer Motion?

Declarative

Define animations with simple props instead of imperative code

Performant

GPU-accelerated transforms and opacity changes

Gestures

Built-in support for drag, hover, and tap interactions

Variants

Reusable animation configurations

Animation Patterns

Fade In on Mount

From src/components/HeroSection.tsx:
import { motion } from "framer-motion"

<motion.div
  initial={{ opacity: 0, y: 40 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.8, ease: "easeOut" }}
  className="max-w-4xl mx-auto text-center"
>
  {/* Content */}
</motion.div>
Properties:
  • initial: Starting state (invisible, 40px down)
  • animate: End state (visible, original position)
  • transition: Animation timing and easing

Staggered Animations

Sequential animations with delays:
<motion.p
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ delay: 0.2, duration: 0.6 }}
  className="text-primary font-heading"
>
  Chief Technology Officer
</motion.p>

<motion.h1
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ delay: 0.4, duration: 0.6 }}
  className="font-heading text-5xl md:text-7xl"
>
  David <span className="text-gradient">Carrascosa</span>
</motion.h1>

<motion.p
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ delay: 0.6, duration: 0.6 }}
  className="text-muted-foreground text-lg"
>
  +20 años liderando tecnología
</motion.p>

Scroll-Triggered Animations

From src/components/AboutSection.tsx:
<motion.div
  initial={{ opacity: 0, y: 30 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true }}
  transition={{ duration: 0.6 }}
>
  <p className="text-primary font-heading">Sobre mí</p>
  <h2 className="font-heading text-3xl md:text-5xl font-bold">
    Tecnología con visión de negocio
  </h2>
</motion.div>
Key Props:
  • whileInView: Animation when element enters viewport
  • viewport={{ once: true }}: Only animate once (performance optimization)

List Item Animations

From src/components/SkillsSection.tsx:
const skills = [
  { icon: Brain, title: "IA Generativa", description: "..." },
  { icon: Code, title: "Desarrollo de Software", description: "..." },
  // ... more items
]

<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
  {skills.map((skill, i) => (
    <motion.div
      key={skill.title}
      initial={{ opacity: 0, y: 20 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ delay: i * 0.1, duration: 0.5 }}
      className="bg-background border border-border rounded-lg p-6"
    >
      <div className="w-11 h-11 rounded-lg bg-primary/10">
        <skill.icon className="w-5 h-5 text-primary" />
      </div>
      <h3 className="font-heading text-lg font-semibold">{skill.title}</h3>
      <p className="text-muted-foreground text-sm">{skill.description}</p>
    </motion.div>
  ))}
</div>
Pattern: Each item animates with incrementing delay (delay: i * 0.1)

Hover Animations

<motion.div
  className="bg-background border border-border rounded-lg p-6"
  whileHover={{ scale: 1.05, borderColor: "hsl(207 90% 54%)" }}
  transition={{ duration: 0.2 }}
>
  {/* Card content */}
</motion.div>

Slide In Animations

From src/components/AboutSection.tsx stats cards:
{stats.map((stat, i) => (
  <motion.div
    key={stat.label}
    initial={{ opacity: 0, x: 20 }}
    whileInView={{ opacity: 1, x: 0 }}
    viewport={{ once: true }}
    transition={{ delay: 0.3 + i * 0.1, duration: 0.5 }}
    className="bg-card border border-border rounded-lg p-5"
  >
    <div className="w-10 h-10 rounded-md bg-primary/10">
      <stat.icon className="w-5 h-5 text-primary" />
    </div>
    <div>
      <p className="font-heading text-2xl font-bold">{stat.value}</p>
      <p className="text-muted-foreground text-sm">{stat.label}</p>
    </div>
  </motion.div>
))}

Tailwind CSS Animations

Defined in tailwind.config.ts:
keyframes: {
  "accordion-down": {
    from: { height: "0" },
    to: { height: "var(--radix-accordion-content-height)" },
  },
  "accordion-up": {
    from: { height: "var(--radix-accordion-content-height)" },
    to: { height: "0" },
  },
  "float": {
    "0%, 100%": { transform: "translateY(0)" },
    "50%": { transform: "translateY(-10px)" },
  },
},
animation: {
  "accordion-down": "accordion-down 0.2s ease-out",
  "accordion-up": "accordion-up 0.2s ease-out",
  "float": "float 6s ease-in-out infinite",
}

Float Animation

Used in the hero section for the scroll indicator:
import { ChevronDown } from "lucide-react"

<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  transition={{ delay: 1.2, duration: 0.6 }}
  className="absolute bottom-10 left-1/2 -translate-x-1/2"
>
  <a href="#about" className="text-muted-foreground/40 hover:text-primary transition-colors">
    <ChevronDown className="w-6 h-6 animate-float" />
  </a>
</motion.div>

Accordion Animations

Used by the Accordion component from shadcn/ui:
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

<Accordion type="single" collapsible>
  <AccordionItem value="item-1">
    <AccordionTrigger>Is it accessible?</AccordionTrigger>
    <AccordionContent>
      Yes. It adheres to the WAI-ARIA design pattern.
    </AccordionContent>
  </AccordionItem>
</Accordion>
The accordion uses animate-accordion-down and animate-accordion-up classes.

Custom Animation Utilities

Defined in src/index.css:
.text-gradient {
  @apply bg-clip-text text-transparent;
  background-image: linear-gradient(135deg, hsl(207 90% 54%), hsl(207 80% 70%));
}

.border-glow {
  border-color: hsl(207 90% 54% / 0.3);
  box-shadow: var(--glow-primary);
}

.section-divider {
  @apply w-full h-px;
  background: linear-gradient(90deg, transparent, hsl(207 90% 54% / 0.3), transparent);
}

Usage Examples

// Gradient text
<h1 className="font-heading text-5xl">
  David <span className="text-gradient">Carrascosa</span>
</h1>

// Glowing border
<div className="bg-card border border-border rounded-lg p-5 border-glow">
  {/* Content */}
</div>

// Section divider
<div className="section-divider mb-8" />

Animation Variants Pattern

Reusable animation configurations:
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
    },
  },
}

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

<motion.div
  variants={containerVariants}
  initial="hidden"
  animate="visible"
>
  {items.map((item) => (
    <motion.div key={item.id} variants={itemVariants}>
      {item.content}
    </motion.div>
  ))}
</motion.div>

Gesture Animations

Drag

<motion.div
  drag
  dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
  dragElastic={0.2}
  className="w-24 h-24 bg-primary rounded-lg cursor-grab active:cursor-grabbing"
/>

Tap

<motion.button
  whileTap={{ scale: 0.95 }}
  className="px-6 py-3 bg-primary text-white rounded-lg"
>
  Click Me
</motion.button>

Hover

<motion.div
  whileHover={{ scale: 1.05, rotate: 2 }}
  transition={{ type: "spring", stiffness: 300 }}
  className="p-6 bg-card rounded-lg cursor-pointer"
>
  Hover over me
</motion.div>

Background Animations

From the hero section:
<section className="relative min-h-screen" style={{ background: "var(--hero-gradient)" }}>
  {/* Grid pattern background */}
  <div
    className="absolute inset-0 opacity-[0.04]"
    style={{
      backgroundImage: `linear-gradient(hsl(207 90% 54%) 1px, transparent 1px), linear-gradient(90deg, hsl(207 90% 54%) 1px, transparent 1px)`,
      backgroundSize: "60px 60px"
    }}
  />

  {/* Blur glow effect */}
  <div
    className="absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-20 blur-[120px]"
    style={{ background: "hsl(207 90% 54%)" }}
  />
</section>

Performance Considerations

Only animate transform and opacity properties for 60fps performance. Avoid animating layout properties like width, height, or margin.

Good (GPU Accelerated)

<motion.div
  animate={{
    opacity: 1,
    x: 0,
    y: 0,
    scale: 1,
    rotate: 0,
  }}
>

Avoid (Causes Layout Reflow)

// Don't do this
<motion.div
  animate={{
    width: "100%",
    height: 200,
    padding: 20,
  }}
>

Optimization Techniques

Only animate on first scroll into view:
<motion.div
  whileInView={{ opacity: 1 }}
  viewport={{ once: true }}
>

Animation Timing

Easing Functions

// Linear
transition={{ ease: "linear" }}

// Ease out (default, good for entrances)
transition={{ ease: "easeOut" }}

// Ease in (good for exits)
transition={{ ease: "easeIn" }}

// Ease in-out (good for loops)
transition={{ ease: "easeInOut" }}

// Custom cubic bezier
transition={{ ease: [0.17, 0.67, 0.83, 0.67] }}

Spring Animations

<motion.div
  animate={{ scale: 1 }}
  transition={{
    type: "spring",
    stiffness: 260,
    damping: 20,
  }}
>

Best Practices

Do

  • Use viewport= for performance
  • Animate transform and opacity only
  • Keep animations under 500ms for interactions
  • Test on low-end devices

Don't

  • Don’t animate layout properties
  • Avoid excessive animation duration
  • Don’t ignore reduced motion preferences
  • Don’t animate everything

Common Animation Recipes

Fade Up on Scroll

<motion.div
  initial={{ opacity: 0, y: 30 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true }}
  transition={{ duration: 0.6 }}
>

Slide In from Left

<motion.div
  initial={{ opacity: 0, x: -50 }}
  animate={{ opacity: 1, x: 0 }}
  transition={{ duration: 0.5 }}
>

Scale on Hover

<motion.div
  whileHover={{ scale: 1.05 }}
  transition={{ duration: 0.2 }}
>

Stagger Children

<motion.div
  initial="hidden"
  animate="visible"
  variants={{
    visible: { transition: { staggerChildren: 0.1 } },
  }}
>
  {items.map((item) => (
    <motion.div
      key={item.id}
      variants={{
        hidden: { opacity: 0, y: 20 },
        visible: { opacity: 1, y: 0 },
      }}
    >
      {item.content}
    </motion.div>
  ))}
</motion.div>

Resources

Build docs developers (and LLMs) love