Seeskou logo

Seeskou

Scrolling animation with SvelteKit

July 6, 2025 8 min read Seeskou
SvelteKitFrontend
Scrolling animation with SvelteKit

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:

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

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: