The portfolio uses a custom ScrollReveal component for scroll-triggered animations, along with CSS animations and transitions for interactive effects.
A reusable wrapper component that triggers animations when elements enter the viewport using the Intersection Observer API.
Implementation
src/components/ScrollReveal.tsx
import { useRef, useEffect, useState, ReactNode } from 'react';
type Variant = 'fade-up' | 'fade-down' | 'fade-left' | 'fade-right' | 'scale';
interface ScrollRevealProps {
children: ReactNode;
className?: string;
variant?: Variant;
delay?: number;
duration?: number;
threshold?: number;
}
const ScrollReveal = ({
children,
className = '',
variant = 'fade-up',
delay = 0,
duration = 700,
threshold = 0.15,
}: ScrollRevealProps) => {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(element);
}
},
{ threshold, rootMargin: '0px 0px -60px 0px' }
);
observer.observe(element);
return () => observer.disconnect();
}, [threshold]);
return (
<div
ref={ref}
className={`scroll-reveal ${variant} ${isVisible ? 'revealed' : ''} ${className}`}
style={{
transitionDuration: `${duration}ms`,
transitionDelay: `${delay}ms`,
}}
>
{children}
</div>
);
};
export default ScrollReveal;
API Reference
The content to animate when scrolled into view
Animation variant to use:
fade-up - Fade in from below
fade-down - Fade in from above
fade-left - Fade in from right
fade-right - Fade in from left
scale - Scale up from center
Delay before animation starts in milliseconds
Animation duration in milliseconds
Percentage of element that must be visible to trigger (0-1)
Additional CSS classes to apply
Usage Examples
Basic
Custom Variant
Staggered
Sequential
import ScrollReveal from "./ScrollReveal";
<ScrollReveal>
<h2>This will fade up when scrolled into view</h2>
</ScrollReveal>
<ScrollReveal variant="fade-left" delay={200}>
<div className="content">
Fades in from the right with 200ms delay
</div>
</ScrollReveal>
<div className="grid lg:grid-cols-2 gap-12">
<ScrollReveal variant="fade-right" delay={200}>
<img src={profilePhoto} alt="Profile" />
</ScrollReveal>
<ScrollReveal variant="fade-left">
<div className="content">
<h2>About Me</h2>
<p>Biography content...</p>
</div>
</ScrollReveal>
</div>
<ScrollReveal>
<h2 className="section-title">Diferenciais</h2>
</ScrollReveal>
{differentials.map((item, i) => (
<ScrollReveal key={i} delay={i * 100}>
<DifferentialCard {...item} />
</ScrollReveal>
))}
How It Works
- Intersection Observer: Monitors when element enters viewport
- Threshold: Element must be 15% visible to trigger (customizable)
- Root Margin:
-60px bottom offset prevents premature triggering
- One-time Animation: Observer disconnects after first trigger
- CSS Classes: Adds
revealed class when visible, triggering CSS transitions
CSS Animation Classes
The ScrollReveal component relies on CSS classes defined in your global styles:
.scroll-reveal {
opacity: 0;
transition-property: opacity, transform;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.scroll-reveal.fade-up {
transform: translateY(30px);
}
.scroll-reveal.fade-down {
transform: translateY(-30px);
}
.scroll-reveal.fade-left {
transform: translateX(30px);
}
.scroll-reveal.fade-right {
transform: translateX(-30px);
}
.scroll-reveal.scale {
transform: scale(0.95);
}
.scroll-reveal.revealed {
opacity: 1;
transform: translateY(0) translateX(0) scale(1);
}
Built-in Animations
The portfolio uses several Tailwind CSS animations:
Fade In
<div
className="animate-fade-in"
style={{ animationDelay: "0.1s" }}
>
<Badge>Vamos construir algo incrível!</Badge>
</div>
Tailwind config:
module.exports = {
theme: {
extend: {
animation: {
'fade-in': 'fadeIn 0.5s ease-in-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
},
},
},
};
Bounce
Used for the WhatsApp floating button:
<button
className={!isOpen ? "animate-bounce" : ""}
style={{ animationDuration: "2s" }}
>
<WhatsAppIcon />
</button>
Pulse
Glowing effect for notifications:
<span className="absolute inset-0 rounded-full bg-[#25D366] animate-ping opacity-20" />
Float
Custom floating animation for memoji:
<img
src={memojiJoia}
className="animate-float"
alt="Memoji"
/>
Tailwind config:
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-10px)' },
},
},
animation: {
'float': 'float 3s ease-in-out infinite',
}
Transition Effects
Scroll-triggered glass effect:
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 50);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
<header
className={`fixed top-0 transition-all duration-300 ${
isScrolled ? "glass py-3" : "py-4 md:py-6"
}`}
>
Glass CSS:
.glass {
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
Hover Effects
<Button className="hover:shadow-[0_0_40px_hsl(24_100%_58%/0.5)] hover:-translate-y-0.5 transition-all duration-300">
Hover Me
</Button>
Animation Delays
Stagger animations for list items:
<div className="space-y-8">
<div className="animate-fade-in" style={{ animationDelay: "0.1s" }}>
<Badge />
</div>
<div className="animate-fade-in" style={{ animationDelay: "0.2s" }}>
<h1>André Ruperto</h1>
</div>
<div className="animate-fade-in" style={{ animationDelay: "0.3s" }}>
<p>Description</p>
</div>
<div className="animate-fade-in" style={{ animationDelay: "0.4s" }}>
<Buttons />
</div>
</div>
Typing Indicator Animation
Custom animation for WhatsApp chat typing:
<div className="flex gap-1.5">
<span
className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce"
style={{ animationDelay: "0ms" }}
/>
<span
className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce"
style={{ animationDelay: "150ms" }}
/>
<span
className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce"
style={{ animationDelay: "300ms" }}
/>
</div>
Accordion Animations
Radix UI provides built-in accordion animations:
className="overflow-hidden text-sm transition-all
data-[state=closed]:animate-accordion-up
data-[state=open]:animate-accordion-down"
Tailwind config:
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' },
},
}
Use will-change sparingly - Only add it to elements that will definitely animate:.scroll-reveal {
will-change: opacity, transform;
}
Intersection Observer caveats:
- Only observes once (disconnects after first trigger)
- Uses
rootMargin: '0px 0px -60px 0px' to prevent bottom-edge triggers
- Default threshold is 0.15 (15% visibility required)
Common Patterns
Section Entrance
<section className="py-20">
<ScrollReveal>
<h2 className="section-title">Section Title</h2>
</ScrollReveal>
<div className="grid grid-cols-2 gap-8">
<ScrollReveal variant="fade-right" delay={100}>
<Card />
</ScrollReveal>
<ScrollReveal variant="fade-left" delay={200}>
<Card />
</ScrollReveal>
</div>
</section>
List Stagger
{items.map((item, i) => (
<ScrollReveal key={item.id} delay={i * 100}>
<ListItem {...item} />
</ScrollReveal>
))}
Grid Animation
<div className="grid md:grid-cols-3 gap-6">
{features.map((feature, i) => (
<ScrollReveal
key={feature.id}
variant="scale"
delay={i * 150}
>
<FeatureCard {...feature} />
</ScrollReveal>
))}
</div>
Next Steps
Component Architecture
Learn about the overall component structure
UI Library
Explore shadcn/ui components and styling