1. Introduction
Scroll-triggered animations are a powerful way to enhance user experience by bringing content to life as users navigate through your website. These animations activate when elements come into view, creating engaging visual feedback that guides attention and improves the overall feel of your application.
In this tutorial, we'll build a reusable Svelte action that uses the Intersection Observer API to trigger animations when elements enter the viewport. This approach is performant, accessible, and highly customizable.
2. Core Concept
We'll create a Svelte action that automatically applies animation classes to elements when they become visible. The pattern looks like this:
<div class="fade-in-hidden" use:intersectionObserver>
Content that animates into view
</div>
When this element enters the viewport, it will automatically receive an animation class that triggers a smooth transition effect.
3. Building the Intersection Observer Action
Let's start by creating our reusable Svelte action:
// lib/actions/intersectionObserver.js
export function intersectionObserver(node, params = {}) {
const {
animationClass = 'animate-show',
observerOptions = {},
once = true,
threshold = 0.1
} = params;
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !entry.target.classList.contains(animationClass)) {
entry.target.classList.add(animationClass);
// If 'once' is true, stop observing after first intersection
if (once) {
observer.unobserve(entry.target);
}
} else if (!entry.isIntersecting && !once) {
// If 'once' is false, remove class when not intersecting
entry.target.classList.remove(animationClass);
}
});
}, { threshold, ...observerOptions });
observer.observe(node);
return {
update(newParams) {
Object.assign(params, newParams);
},
destroy() {
observer.disconnect();
}
};
}
4. Key Features:
- animationClass: The CSS class applied when the element becomes visible
- once: Whether the animation should only happen once (default: true)
- threshold: How much of the element must be visible to trigger (0.1 = 10%)
- rootMargin: Margin around the root viewport for early/late triggering
- observerOptions: Additional Intersection Observer options
5. Creating Animation Styles
Now let's create CSS classes for our animations. These must be defined in a global CSS file since Svelte's scoped styling won't work with dynamically added classes:
/* styles/animations.css */
/* Fade in from bottom */
.fade-in-hidden {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.fade-in-visible {
opacity: 1;
transform: translateY(0);
}
/* Slide in from left */
.slide-left-hidden {
opacity: 0;
transform: translateX(-50px);
transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}
.slide-left-visible {
opacity: 1;
transform: translateX(0);
}
/* Scale up effect */
.scale-hidden {
opacity: 0;
transform: scale(0.8);
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
}
.scale-visible {
opacity: 1;
transform: scale(1);
}
/* Staggered animations for multiple elements */
.stagger-1 { transition-delay: 0.1s; }
.stagger-2 { transition-delay: 0.2s; }
.stagger-3 { transition-delay: 0.3s; }
.stagger-4 { transition-delay: 0.4s; }
Oh, and don't forget to add the prefers-reduced-motion media query to respect users' preferences:
/* animations.css */
/* ... existing styles ... */
@media (prefers-reduced-motion: reduce) {
.fade-in-hidden,
.fade-in-visible,
.slide-left-hidden,
.slide-left-visible,
.scale-hidden,
.scale-visible {
transition: none !important;
transform: none !important;
opacity: 1 !important;
animation: none !important;
}
}
Note: animation classes can't be defined directly in the Svelte component's style block, as Svelte will not apply them correctly due to the way it handles scoped styles. Instead, define them in a separate CSS file and import that file into your Svelte component or global styles.
6. Usage Examples
6.1 Basic Usage
<script>
import { intersectionObserver } from '$lib/actions/intersectionObserver.js';
</script>
<section class="fade-in-hidden" use:intersectionObserver={{
animationClass: 'fade-in-visible'
}}>
<h2>Welcome to Our Service</h2>
<p>This content will fade in smoothly when you scroll to it.</p>
</section>
6.2 Slide-in Animation with Custom Threshold
<div class="slide-left-hidden" use:intersectionObserver={{
animationClass: 'slide-left-visible',
threshold: 0.3,
rootMargin: '50px'
}}>
<h3>Feature Highlight</h3>
<p>This slides in from the left when 30% is visible, with a 50px margin.</p>
</div>
6.3 Repeating Animation
<div class="scale-hidden" use:intersectionObserver={{
animationClass: 'scale-visible',
once: false,
threshold: 0.5
}}>
<h3>Interactive Element</h3>
<p>This will animate every time it enters and leaves the viewport.</p>
</div>
6.4 Staggered Animations
<div class="feature-grid">
<div class="fade-in-hidden stagger-1" use:intersectionObserver={{
animationClass: 'fade-in-visible'
}}>
<h4>Feature 1</h4>
</div>
<div class="fade-in-hidden stagger-2" use:intersectionObserver={{
animationClass: 'fade-in-visible'
}}>
<h4>Feature 2</h4>
</div>
<div class="fade-in-hidden stagger-3" use:intersectionObserver={{
animationClass: 'fade-in-visible'
}}>
<h4>Feature 3</h4>
</div>
</div>
7. Performance Tips
- Use threshold wisely: Lower values (0.1) trigger earlier but may cause animations to fire too frequently
- Leverage rootMargin: Use negative values to delay animations until elements are more centered in the viewport
- Consider once: false carefully: Repeating animations can be distracting if overused
- Batch similar animations: Use the same observer settings for similar elements to reduce memory usage
8. Conclusion
This Intersection Observer-based approach provides a robust foundation for scroll-triggered animations in Svelte applications. The action is reusable, performant, and respects user accessibility preferences. By combining it with thoughtful CSS animations, you can create engaging user experiences that feel polished and professional.
The key benefits of this approach are:
- Reusability: One action works across your entire application
- Performance: Uses the efficient Intersection Observer API
- Accessibility: Respects motion preferences
- Flexibility: Highly customizable through parameters
- Clean separation: Logic in JavaScript, styling in CSS