Skip to main content
The portfolio uses a custom ScrollReveal component for scroll-triggered animations, along with CSS animations and transitions for interactive effects.

ScrollReveal Component

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

children
ReactNode
required
The content to animate when scrolled into view
variant
string
default:"fade-up"
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
number
default:0
Delay before animation starts in milliseconds
duration
number
default:700
Animation duration in milliseconds
threshold
number
Percentage of element that must be visible to trigger (0-1)
className
string
Additional CSS classes to apply

Usage Examples

import ScrollReveal from "./ScrollReveal";

<ScrollReveal>
  <h2>This will fade up when scrolled into view</h2>
</ScrollReveal>

How It Works

  1. Intersection Observer: Monitors when element enters viewport
  2. Threshold: Element must be 15% visible to trigger (customizable)
  3. Root Margin: -60px bottom offset prevents premature triggering
  4. One-time Animation: Observer disconnects after first trigger
  5. 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:
styles/animations.css
.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

HeroSection.tsx
<div 
  className="animate-fade-in" 
  style={{ animationDelay: "0.1s" }}
>
  <Badge>Vamos construir algo incrível!</Badge>
</div>
Tailwind config:
tailwind.config.js
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:
WhatsAppButton.tsx
<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:
AboutSection.tsx
<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

Glassmorphism Header

Scroll-triggered glass effect:
Header.tsx
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:
HeroSection.tsx
<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:
WhatsAppButton.tsx
<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:
accordion.tsx
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' },
  },
}

Performance Tips

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

Build docs developers (and LLMs) love