Scroll Animations

Add reveal animations to landing components as they scroll into view.

useScrollAnimation Hook

Components use the useScrollAnimation hook for reveal animations:

import { useScrollAnimation } from '@/hooks';

export default function MySection() {
  const { elementRef, isVisible } = useScrollAnimation();

  return (
    <section
      ref={elementRef as React.RefObject<HTMLElement>}
      className={`scroll-fade-in ${isVisible ? 'visible' : ''}`}
    >
      {/* Content */}
    </section>
  );
}

CSS Classes

Add these animation classes to globals.css:

/* Fade in from bottom */
.scroll-fade-in {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.scroll-fade-in.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Slide in from left */
.scroll-slide-left {
  opacity: 0;
  transform: translateX(-30px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.scroll-slide-left.visible {
  opacity: 1;
  transform: translateX(0);
}

/* Slide in from right */
.scroll-slide-right {
  opacity: 0;
  transform: translateX(30px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.scroll-slide-right.visible {
  opacity: 1;
  transform: translateX(0);
}

/* Scale up */
.scroll-scale {
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.scroll-scale.visible {
  opacity: 1;
  transform: scale(1);
}

Hook Implementation

Ensure the hook is exported from src/hooks/index.ts:

export { useScrollAnimation } from './useScrollAnimation';

The hook implementation using Intersection Observer:

// src/hooks/useScrollAnimation.ts
import { useEffect, useRef, useState } from 'react';

export function useScrollAnimation(threshold = 0.1) {
  const elementRef = useRef<HTMLElement>(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const element = elementRef.current;
    if (!element) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.unobserve(element); // Only animate once
        }
      },
      { threshold }
    );

    observer.observe(element);

    return () => observer.disconnect();
  }, [threshold]);

  return { elementRef, isVisible };
}

Reduced Motion Support

Respect user preferences for reduced motion:

@media (prefers-reduced-motion: reduce) {
  .scroll-fade-in,
  .scroll-slide-left,
  .scroll-slide-right,
  .scroll-scale {
    transition: none;
    opacity: 1;
    transform: none;
  }
}

Staggered Animations

For lists or grids, add staggered delays:

{items.map((item, index) => (
  <div
    key={index}
    className={`scroll-fade-in ${isVisible ? 'visible' : ''}`}
    style={{ transitionDelay: `${index * 100}ms` }}
  >
    {item.content}
  </div>
))}

Troubleshooting

Animations not triggering

  1. Verify CSS is in globals.css
  2. Check hook is properly imported
  3. Ensure elementRef is attached to the element
  4. Check that visible class is being applied

Animations too fast/slow

Adjust the transition duration:

.scroll-fade-in {
  transition: opacity 0.8s ease-out, transform 0.8s ease-out; /* slower */
}

Elements flash on load

Add initial state to prevent flash:

.scroll-fade-in {
  opacity: 0; /* Start hidden */
}