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
- Verify CSS is in
globals.css - Check hook is properly imported
- Ensure
elementRefis attached to the element - Check that
visibleclass 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 */
}