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.
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 >
);
};
Dynamic Speed : Fast (20s), Normal (40s), Slow (80s)
Direction Control : Left (forwards) or Right (reverse)
Pause on Hover : Animation pauses when user hovers
CSS Variables : Runtime control of animation properties
Infinite Loop : Seamless continuous scrolling
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
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.
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
} }
/>
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
Class Effect Duration animate-pulseFade in/out 2s animate-scrollHorizontal scroll 40s (configurable) animate-spotlightMoving spotlight Custom animate-inEntrance animation 150ms transition-allAll properties 150ms duration-{n}Custom duration ms
Next Steps
Tailwind Configuration Review the complete Tailwind setup
Theme Customization Customize colors and styling