A complete, accessible, and performant scroll-triggered animation system for your personal website with reversible animations.
✅ Reversible Animations: Elements animate in AND out every time they enter/leave viewport
✅ Smooth Transitions: Fade in + slide up with professional easing
✅ Smart Triggering: Animates when elements are 20% visible
✅ Infinite Repeatability: Works every time you scroll past content
✅ Accessibility: Respects prefers-reduced-motion
✅ Performance: Hardware-accelerated CSS transforms
✅ Fallback: Elements visible by default if JS fails
✅ React Ready: Works seamlessly with React components
✅ Multiple Variants: Different animation styles available
✅ No Teleporting: Smooth transitions from hidden to visible state
Add the data-animate="fade-up" attribute to any element you want to animate:
<div data-animate="fade-up">
<h1>This will fade up when scrolled into view</h1>
</div>
<section data-animate="fade-up">
<p>This section will also animate</p>
</section>The animations now work in both directions:
- Scroll Down: Elements fade in and slide up when entering viewport
- Scroll Up: Elements fade out and slide down when leaving viewport
- Repeat: This works infinitely - every time you scroll past content
<div data-animate="fade-up">
<!-- 32px slide up/down, 400ms duration -->
</div><div data-animate="fade-up-subtle">
<!-- 16px slide up/down, 300ms duration -->
</div><div data-animate="fade-up-slow">
<!-- 48px slide up/down, 600ms duration -->
</div>The system automatically works with React components. Just add the data attribute:
function MyComponent() {
return (
<div data-animate="fade-up" className="my-component">
<h2>Animated React Component</h2>
<p>This will animate when scrolled into view</p>
</div>
);
}The system uses a smart CSS approach to ensure smooth reversible animations:
- Elements start hidden: All animated elements begin with
opacity: 0andtransform: translateY(32px) - Transition is always active: The CSS transition is applied immediately, not when the element becomes visible
- Two states: Elements smoothly transition between hidden (
.fade-up-init) and visible (.is-visible) states
[data-animate="fade-up"] {
/* Transition is always active */
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
/* Start hidden */
opacity: 0;
transform: translateY(32px);
}
[data-animate="fade-up"].is-visible {
/* Smoothly transition to visible */
opacity: 1;
transform: translateY(0);
}
[data-animate="fade-up"].fade-up-init {
/* Smoothly transition back to hidden */
opacity: 0;
transform: translateY(32px);
}- IntersectionObserver: Watches for elements entering AND leaving the viewport
- Reversible Logic:
- When
isIntersectingistrue: Add.is-visibleclass (animate in) - When
isIntersectingisfalse: Add.fade-up-initclass (animate out)
- When
- Continuous Observation: Elements are never unobserved, allowing infinite repetition
- Immediate Response: No stagger delays for individual elements
You can customize the animation behavior by passing options:
import { initFadeUpAnimations } from './utils/scrollFadeUp';
initFadeUpAnimations({
stagger: 0, // No stagger for reversible animations
threshold: 0.2, // Trigger when 20% visible
rootMargin: '0px 0px -10% 0px' // Custom trigger area
});| Option | Default | Description |
|---|---|---|
stagger |
0 |
No stagger for reversible animations |
threshold |
0.2 |
Trigger when element is this % visible (0-1) |
rootMargin |
'0px 0px -10% 0px' |
Custom trigger area (CSS margin format) |
The animations use smooth cubic-bezier easing curves:
/* Standard: 400ms with smooth easing */
[data-animate="fade-up"] {
transition:
opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Subtle: 300ms for faster feel */
[data-animate="fade-up-subtle"] {
transition:
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Slow: 600ms for dramatic effect */
[data-animate="fade-up-slow"] {
transition:
opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}Add your own animation variants:
[data-animate="fade-up-custom"] {
transition:
opacity 0.5s ease-out,
transform 0.5s ease-out;
opacity: 0;
transform: translateY(24px) scale(0.95);
}
[data-animate="fade-up-custom"].is-visible {
opacity: 1;
transform: translateY(0) scale(1);
}
[data-animate="fade-up-custom"].fade-up-init {
opacity: 0;
transform: translateY(24px) scale(0.95);
}The system automatically respects user preferences:
prefers-reduced-motion: reduce: Animations are disabled, elements appear instantlyprefers-reduced-motion: no-preference: Full animations with hardware acceleration
- Elements are visible by default (no content hiding)
- Animations don't interfere with screen readers
- No ARIA attributes needed
- Hardware Acceleration: Uses
transformandopacityfor GPU acceleration - CSS Containment:
contain: layout style paintprevents layout thrashing - Efficient Observers: IntersectionObserver with passive listeners
- Continuous Observation: Elements stay observed for infinite repetition
- Immediate Response: No delays for smooth user experience
- Don't overuse: Limit animations to key content sections
- Consider performance: Reversible animations can be more CPU intensive
- Test on mobile: Ensure smooth performance on lower-end devices
- Modern Browsers: Full support (Chrome 51+, Firefox 55+, Safari 12.1+)
- IntersectionObserver: Required for animations
- Fallback: Elements visible by default in unsupported browsers
- Check console: Look for initialization messages
- Verify data attributes: Ensure
data-animate="fade-up"is present - Check IntersectionObserver support: Should work in all modern browsers
- Reduce animation count: Limit the number of animated elements
- Use subtle variants: Try
fade-up-subtlefor less movement - Test scroll speed: Very fast scrolling might cause performance issues
- Dynamic content: Use
reinitFadeUpAnimations()after content changes - Route changes: Animations reinitialize automatically on navigation
- Component updates: Use the
useFadeUpAnimationhook for manual control
function BlogList({ posts }) {
return (
<div className="blog-list">
{posts.map((post, index) => (
<article
key={post.id}
data-animate="fade-up"
className="blog-card"
>
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}function ProjectGrid({ projects }) {
return (
<div className="project-grid">
{projects.map((project, index) => (
<div
key={project.id}
data-animate="fade-up-slow"
className="project-card"
>
<img src={project.image} alt={project.title} />
<h3>{project.title}</h3>
<p>{project.description}</p>
</div>
))}
</div>
);
}function HeroSection() {
return (
<section className="hero">
<h1 data-animate="fade-up">Welcome to My Portfolio</h1>
<p data-animate="fade-up-subtle">Frontend Developer & Designer</p>
<button data-animate="fade-up-slow" className="cta-button">
View My Work
</button>
</section>
);
}Initialize the animation system with reversible animations.
.is-visible: Animated visible state.fade-up-init: Animated hidden state
data-animate="fade-up": Standard reversible animationdata-animate="fade-up-subtle": Subtle reversible animationdata-animate="fade-up-slow": Slow reversible animation
- CSS-First: Transitions are defined in CSS, not JavaScript
- State-Based: Elements have clear hidden/visible states
- Performance: Uses hardware-accelerated properties
- Reversible: IntersectionObserver tracks both entry and exit
- Accessible: Respects user motion preferences
- Element starts with
opacity: 0, transform: translateY(32px) - CSS transition is always active
- IntersectionObserver detects element entering viewport
.is-visibleclass added → element animates toopacity: 1, transform: translateY(0)- IntersectionObserver detects element leaving viewport
.fade-up-initclass added → element animates back toopacity: 0, transform: translateY(32px)- Process repeats infinitely
This animation system provides a professional, accessible, and performant way to add reversible scroll-triggered animations to your personal website. The implementation ensures smooth fade-up animations that work in both directions, creating a dynamic and engaging user experience.