Skip to main content

Overview

This portfolio uses a combination of Tailwind CSS animations and Framer Motion for sophisticated interactive effects. The animation system includes custom scroll animations, typewriter effects, and smooth transitions.

Framer Motion Integration

Framer Motion v11.3.8 is used for complex animations and gestures throughout the portfolio.

Installation

From package.json:16:
{
  "dependencies": {
    "framer-motion": "^11.3.8"
  }
}

Basic Usage

Framer Motion components are used extensively in UI components:
import { motion } from "framer-motion";

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5 }}
>
  Content
</motion.div>
Framer Motion provides a declarative API for creating smooth, performant animations that work across all modern browsers.

Custom Scroll Animation

The portfolio includes a custom infinite scroll animation defined in tailwind.config.ts:18:

Configuration

animation: {
  scroll:
    "scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite",
},
keyframes: {
  scroll: {
    to: {
      transform: "translate(calc(-50% - 0.5rem))",
    },
  },
},

Implementation Example

Used in src/app/components/ui/infinite-moving-cars.tsx:87:
export const InfiniteMovingCards = ({
  items,
  direction = "left",
  speed = "fast",
  pauseOnHover = true,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const [start, setStart] = useState(false);

  const getDirection = useCallback(() => {
    if (containerRef.current) {
      if (direction === "left") {
        containerRef.current.style.setProperty(
          "--animation-direction",
          "forwards"
        );
      } else {
        containerRef.current.style.setProperty(
          "--animation-direction",
          "reverse"
        );
      }
    }
  }, [direction]);

  const getSpeed = useCallback(() => {
    if (containerRef.current) {
      if (speed === "fast") {
        containerRef.current.style.setProperty(
          "--animation-duration", 
          "20s"
        );
      } else if (speed === "normal") {
        containerRef.current.style.setProperty(
          "--animation-duration", 
          "40s"
        );
      } else {
        containerRef.current.style.setProperty(
          "--animation-duration", 
          "80s"
        );
      }
    }
  }, [speed]);

  return (
    <ul
      className={cn(
        "flex min-w-full",
        start && "animate-scroll",
        pauseOnHover && "hover:[animation-play-state:paused]"
      )}
    >
      {items.map((item) => (
        <li key={item.name}>{item.quote}</li>
      ))}
    </ul>
  );
};
The scroll animation uses CSS variables to allow dynamic configuration without rebuilding styles, making it highly reusable and performant.

Tailwind Built-in Animations

The portfolio uses several built-in Tailwind animations:

Pulse Animation

Used in src/app/components/sections/Projects.tsx:34 for loading states:
<div className="animate-pulse">
  Loading...
</div>

<button className="animate-pulse hover:animate-none">
  Call to Action
</button>

Animation Control

// Stop animation on hover
<div className="animate-pulse hover:animate-none">
  Pulses until hovered
</div>

Framer Motion Animations

Typewriter Effect

Implemented in src/app/components/ui/type-writer-effect.tsx:4:
import { motion, stagger, useAnimate, useInView } from "framer-motion";

export const TypewriterEffect = ({ words }) => {
  const [scope, animate] = useAnimate();
  const isInView = useInView(scope);

  useEffect(() => {
    if (isInView) {
      animate(
        "span",
        {
          opacity: 1,
          // Animation config
        },
        {
          duration: 0.3,
          delay: stagger(0.1),
        }
      );
    }
  }, [isInView, animate]);

  return (
    <motion.div ref={scope}>
      {words.map((word) => (
        <motion.span
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
        >
          {word}
        </motion.span>
      ))}
    </motion.div>
  );
};
Features:
  • Staggered letter animations
  • View-based triggering with useInView
  • Customizable timing and delay

Sticky Scroll Reveal

Implemented in src/app/components/ui/sticky-scroll-reveal.tsx:3:
import { useMotionValueEvent, useScroll } from "framer-motion";
import { motion } from "framer-motion";

export const StickyScroll = ({ content }) => {
  const { scrollYProgress } = useScroll();

  return (
    <motion.div
      animate={{
        // Scroll-based animations
      }}
    >
      <motion.h2
        animate={{
          opacity: scrollYProgress,
        }}
      >
        Title
      </motion.h2>
      <motion.p
        animate={{
          opacity: scrollYProgress,
        }}
      >
        Content
      </motion.p>
    </motion.div>
  );
};
Features:
  • Scroll progress tracking
  • Smooth opacity transitions
  • Content reveal on scroll

Background Gradient Animation

Implemented in src/app/components/ui/background-gradient.tsx:3:
import { motion } from "framer-motion";

export const BackgroundGradient = ({
  children,
  animate = true,
}) => {
  const variants = {
    initial: {
      backgroundPosition: "0 50%",
    },
    animate: {
      backgroundPosition: ["0, 50%", "100% 50%", "0 50%"],
    },
  };

  return (
    <motion.div
      variants={animate ? variants : undefined}
      initial={animate ? "initial" : undefined}
      animate={animate ? "animate" : undefined}
      transition={{
        duration: 5,
        repeat: Infinity,
        repeatType: "reverse",
      }}
      style={{
        backgroundSize: animate ? "400% 400%" : undefined,
      }}
    >
      {children}
    </motion.div>
  );
};
Features:
  • Animated gradient backgrounds
  • Optional animation toggle
  • Infinite loop with reverse
Animated gradients can be performance-intensive. Use sparingly and test on lower-end devices.

Transition Classes

Tailwind transition utilities are used throughout for smooth state changes:

Hover Transitions

From src/app/components/MobileNav.tsx:43:
<a className="
  hover:bg-spotify-green/10 
  rounded-lg 
  transition-all 
  duration-200 
  ease-in-out 
  hover:translate-x-2
">
  Link
</a>
Breakdown:
  • transition-all: Animates all properties
  • duration-200: 200ms duration
  • ease-in-out: Smooth easing function
  • hover:translate-x-2: Slide effect on hover

Animation Utilities

From src/app/components/MobileNav.tsx:36:
<div className="
  animate-in 
  slide-in-from-top-2 
  duration-300 
  ease-out
">
  Content
</div>
These utilities are part of Tailwind’s animation system and provide entrance/exit animations without Framer Motion.

Spotlight Animation

Custom spotlight effect in src/app/components/ui/Spotlight.tsx:13:
<div className="
  animate-spotlight 
  pointer-events-none 
  absolute 
  z-[1] 
  h-[169%] 
  w-[138%] 
  opacity-100
" />
This creates a moving spotlight effect that follows user interaction or scrolling.

Moving Border Effect

Implemented in src/app/components/ui/moving-border.tsx:9:
import { motion } from "framer-motion";

export const MovingBorder = ({ children }) => {
  return (
    <motion.div
      // Animated border implementation
    >
      {children}
    </motion.div>
  );
};
Creates an animated border that moves around the element’s perimeter.

Performance Best Practices

1. Use CSS Animations for Simple Effects

// Good - CSS animation
<div className="animate-pulse" />

// Overkill - Framer Motion for simple effect
<motion.div animate={{ opacity: [0.5, 1, 0.5] }} />

2. Optimize Framer Motion

// Use layout animations sparingly
<motion.div layout /> // Can be expensive

// Prefer transform and opacity
<motion.div 
  animate={{ 
    x: 100,      // GPU accelerated
    opacity: 0.5 // GPU accelerated
  }} 
/>

3. Debounce Scroll Listeners

import { useScroll } from "framer-motion";
import { useThrottle } from "@/hooks/useThrottle";

const { scrollY } = useScroll();
const throttledScrollY = useThrottle(scrollY, 100);
Always test animations on mobile devices. What looks smooth on desktop may struggle on lower-powered devices.

Animation Utilities Reference

ClassEffectDuration
animate-pulseFade in/out2s
animate-scrollHorizontal scroll40s (configurable)
animate-spotlightMoving spotlightCustom
animate-inEntrance animation150ms
transition-allAll properties150ms
duration-{n}Custom durationms

Next Steps

Tailwind Configuration

Review the complete Tailwind setup

Theme Customization

Customize colors and styling

Build docs developers (and LLMs) love