DEV Community

Lucas Bennett
Lucas Bennett

Posted on

Advanced Animation Techniques with svelte-animations in Svelte

svelte-animations is a comprehensive collection of animation components for Svelte applications, built with Tailwind CSS and svelte-motion. It provides a rich set of pre-built animated components and utilities that enable developers to create sophisticated, performant animations with minimal effort. This guide walks through advanced animation techniques, custom animation patterns, and complex animation orchestration using svelte-animations with Svelte. This article is part 21 of a series on using svelte-animations with Svelte.

Prerequisites

Before you begin, make sure you have:

  • Node.js version 18 or higher installed
  • npm, pnpm, or yarn package manager
  • SvelteKit project set up (or any Svelte project with build tooling)
  • Tailwind CSS configured in your project
  • A solid understanding of Svelte reactivity, components, and lifecycle hooks
  • Familiarity with CSS animations and transitions
  • Basic knowledge of svelte-motion (the underlying animation library)

For this advanced tutorial, we'll work with complex animation patterns including:

  • Animation orchestration: Coordinating multiple animations together
  • Custom animation variants: Creating reusable animation patterns
  • Advanced transitions: Complex enter/exit animations with AnimatePresence
  • Performance optimization: Optimizing animations for smooth performance
  • SVG animations: Animating SVG properties with svelte-motion

Installation

Install the svelte-animations package using npm:

npm install @svelte-animations/aceternity-ui
Enter fullscreen mode Exit fullscreen mode

Or with pnpm:

pnpm add @svelte-animations/aceternity-ui
Enter fullscreen mode Exit fullscreen mode

Or with yarn:

yarn add @svelte-animations/aceternity-ui
Enter fullscreen mode Exit fullscreen mode

You'll also need to install svelte-motion as a peer dependency:

npm install svelte-motion
Enter fullscreen mode Exit fullscreen mode

After installation, your package.json should include:

{
  "dependencies": {
    "@svelte-animations/aceternity-ui": "^1.0.0",
    "svelte-motion": "^0.10.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Project Setup

Ensure Tailwind CSS is properly configured in your project. If you're using SvelteKit, add the svelte-animations path to your tailwind.config.js:

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./src/**/*.{html,js,svelte,ts}",
    "./node_modules/@svelte-animations/**/*.{js,svelte}"
  ],
  theme: {
    extend: {
      // Add custom animations if needed
      animation: {
        'aurora': 'aurora 60s linear infinite',
      },
      keyframes: {
        aurora: {
          '0%': { backgroundPosition: '50% 50%, 50% 50%' },
          '100%': { backgroundPosition: '350% 50%, 350% 50%' },
        },
      },
    },
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Create a utility file for component imports (optional but recommended):

// src/lib/animations/index.ts
export { default as AnimatedTestimonials } from '@svelte-animations/aceternity-ui/components/animated-testimonials/AnimatedTestimonials.svelte';
export { default as BackgroundLines } from '@svelte-animations/aceternity-ui/components/background-lines/BackgroundLines.svelte';
export { default as AuroraBackground } from '@svelte-animations/aceternity-ui/components/aurora-background/AuroraBackground.svelte';
// Export other components as needed
Enter fullscreen mode Exit fullscreen mode

First Example / Basic Usage

Let's start with a simple example that demonstrates the basic usage of svelte-motion (the core animation library):

<!-- src/components/SimpleAnimation.svelte -->
<script>
  import { Motion } from "svelte-motion";

  let isVisible = true;
</script>

<div class="p-8">
  <button 
    on:click={() => isVisible = !isVisible}
    class="px-4 py-2 bg-blue-500 text-white rounded mb-4"
  >
    Toggle Animation
  </button>

  {#if isVisible}
    <Motion
      initial={{ opacity: 0, y: -20 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: 20 }}
      transition={{ duration: 0.3, ease: "easeOut" }}
      let:motion
    >
      <div 
        use:motion
        class="p-4 bg-gray-100 rounded-lg shadow-md"
      >
        <h2 class="text-xl font-bold">Animated Content</h2>
        <p>This content animates in and out smoothly.</p>
      </div>
    </Motion>
  {/if}
</div>
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • Motion component: The core animation component from svelte-motion
  • Initial state: initial prop defines the starting animation state
  • Animate state: animate prop defines the target animation state
  • Exit state: exit prop defines how the element animates out
  • Transition: Controls timing, easing, and duration
  • use:motion: The action directive that applies the animation to the element

Understanding the Basics

How svelte-animations Integrates with Svelte

svelte-animations is built on top of svelte-motion, which provides a declarative API for animations in Svelte:

  1. Motion Component: Wraps elements to enable animations
  2. AnimatePresence: Manages enter/exit animations for conditional rendering
  3. Variants: Reusable animation state objects
  4. Reactivity: Animations automatically update when reactive values change
  5. Performance: Uses CSS transforms and opacity for hardware-accelerated animations

Animation Variants Pattern

Variants allow you to define reusable animation states:

<script>
  import { Motion } from "svelte-motion";

  const fadeInUp = {
    initial: { opacity: 0, y: 20 },
    animate: { opacity: 1, y: 0 },
    exit: { opacity: 0, y: -20 }
  };

  const scaleIn = {
    initial: { opacity: 0, scale: 0.8 },
    animate: { opacity: 1, scale: 1 },
    exit: { opacity: 0, scale: 0.8 }
  };

  let animationType = 'fadeInUp';
</script>

<Motion
  variants={animationType === 'fadeInUp' ? fadeInUp : scaleIn}
  initial="initial"
  animate="animate"
  exit="exit"
  transition={{ duration: 0.4 }}
  let:motion
>
  <div use:motion class="p-6 bg-blue-100 rounded">
    Content with {animationType} animation
  </div>
</Motion>
Enter fullscreen mode Exit fullscreen mode

AnimatePresence for Enter/Exit Animations

AnimatePresence manages animations when elements are added or removed from the DOM:

<script>
  import { Motion, AnimatePresence } from "svelte-motion";

  let items = [
    { id: 1, text: "Item 1" },
    { id: 2, text: "Item 2" },
    { id: 3, text: "Item 3" }
  ];

  function removeItem(id) {
    items = items.filter(item => item.id !== id);
  }
</script>

<div class="space-y-2">
  <AnimatePresence let:item list={items}>
    {#each items as item (item.id)}
      <Motion
        initial={{ opacity: 0, x: -20 }}
        animate={{ opacity: 1, x: 0 }}
        exit={{ opacity: 0, x: 20 }}
        transition={{ duration: 0.3 }}
        let:motion
      >
        <div 
          use:motion
          class="flex items-center justify-between p-4 bg-gray-100 rounded"
        >
          <span>{item.text}</span>
          <button 
            on:click={() => removeItem(item.id)}
            class="px-3 py-1 bg-red-500 text-white rounded"
          >
            Remove
          </button>
        </div>
      </Motion>
    {/each}
  </AnimatePresence>
</div>
Enter fullscreen mode Exit fullscreen mode

Practical Example / Building Something Real

Let's build a sophisticated animated dashboard with multiple coordinated animations:

<!-- src/components/AnimatedDashboard.svelte -->
<script>
  import { Motion, AnimatePresence } from "svelte-motion";
  import { onMount } from "svelte";

  let activeCard = 0;
  let cards = [
    { id: 1, title: "Sales", value: "$12,450", change: "+12%", color: "bg-blue-500" },
    { id: 2, title: "Users", value: "1,234", change: "+8%", color: "bg-green-500" },
    { id: 3, title: "Revenue", value: "$45,678", change: "+15%", color: "bg-purple-500" },
    { id: 4, title: "Orders", value: "567", change: "+5%", color: "bg-orange-500" }
  ];

  let notifications = [];
  let notificationId = 0;

  // Stagger animation variants
  const cardVariants = {
    initial: { opacity: 0, y: 20, scale: 0.95 },
    animate: (index) => ({
      opacity: 1,
      y: 0,
      scale: 1,
      transition: {
        delay: index * 0.1,
        duration: 0.4,
        ease: "easeOut"
      }
    })
  };

  // Notification animation
  const notificationVariants = {
    initial: { opacity: 0, x: 300, scale: 0.8 },
    animate: { 
      opacity: 1, 
      x: 0, 
      scale: 1,
      transition: { type: "spring", stiffness: 300, damping: 30 }
    },
    exit: { 
      opacity: 0, 
      x: 300, 
      scale: 0.8,
      transition: { duration: 0.2 }
    }
  };

  function addNotification(message) {
    const id = notificationId++;
    notifications = [...notifications, { id, message, timestamp: Date.now() }];

    // Auto-remove after 3 seconds
    setTimeout(() => {
      notifications = notifications.filter(n => n.id !== id);
    }, 3000);
  }

  function nextCard() {
    activeCard = (activeCard + 1) % cards.length;
    addNotification(`Switched to ${cards[activeCard].title}`);
  }

  onMount(() => {
    // Simulate periodic updates
    const interval = setInterval(() => {
      nextCard();
    }, 5000);

    return () => clearInterval(interval);
  });
</script>

<div class="min-h-screen bg-gray-50 p-8">
  <div class="max-w-7xl mx-auto">
    <!-- Header with animated title -->
    <Motion
      initial={{ opacity: 0, y: -20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
      let:motion
    >
      <h1 
        use:motion
        class="text-4xl font-bold mb-8 text-gray-900"
      >
        Animated Dashboard
      </h1>
    </Motion>

    <!-- Cards grid with stagger animation -->
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
      {#each cards as card, index}
        <Motion
          variants={cardVariants}
          initial="initial"
          animate="animate"
          custom={index}
          let:motion
        >
          <div 
            use:motion
            class="p-6 bg-white rounded-lg shadow-lg hover:shadow-xl transition-shadow cursor-pointer {card.color}"
            on:click={() => { activeCard = index; addNotification(`Selected ${card.title}`); }}
            role="button"
            tabindex="0"
          >
            <h3 class="text-white text-sm font-medium mb-2">{card.title}</h3>
            <p class="text-white text-3xl font-bold mb-1">{card.value}</p>
            <p class="text-white text-sm opacity-90">{card.change}</p>
          </div>
        </Motion>
      {/each}
    </div>

    <!-- Active card detail with complex animation -->
    <AnimatePresence mode="wait">
      {#key activeCard}
        <Motion
          key={`card-${activeCard}`}
          initial={{ opacity: 0, rotateX: -90 }}
          animate={{ opacity: 1, rotateX: 0 }}
          exit={{ opacity: 0, rotateX: 90 }}
          transition={{ 
            duration: 0.5,
            ease: "easeInOut"
          }}
          let:motion
        >
          <div 
            use:motion
            class="p-8 bg-white rounded-lg shadow-lg mb-8"
            style="transform-style: preserve-3d;"
          >
            <h2 class="text-2xl font-bold mb-4">{cards[activeCard].title} Details</h2>
            <p class="text-gray-600">Current value: <span class="font-bold">{cards[activeCard].value}</span></p>
            <p class="text-gray-600 mt-2">Change: <span class="font-bold text-green-600">{cards[activeCard].change}</span></p>
          </div>
        </Motion>
      {/key}
    </AnimatePresence>

    <!-- Notification stack -->
    <div class="fixed top-4 right-4 z-50 space-y-2">
      <AnimatePresence let:item list={notifications}>
        {#each notifications as notification (notification.id)}
          <Motion
            variants={notificationVariants}
            initial="initial"
            animate="animate"
            exit="exit"
            let:motion
          >
            <div 
              use:motion
              class="p-4 bg-white rounded-lg shadow-xl border-l-4 border-blue-500 min-w-[300px]"
            >
              <p class="text-sm font-medium text-gray-900">{notification.message}</p>
            </div>
          </Motion>
        {/each}
      </AnimatePresence>
    </div>

    <!-- Control button -->
    <Motion
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      let:motion
    >
      <button 
        use:motion
        on:click={nextCard}
        class="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
      >
        Next Card
      </button>
    </Motion>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • Staggered animations: Cards animate in sequence with delays
  • 3D transforms: Using rotateX for card flip effect
  • Spring animations: Natural, physics-based motion for notifications
  • Coordinated animations: Multiple elements animating together
  • Interactive animations: Hover and tap states with whileHover and whileTap
  • AnimatePresence: Managing enter/exit animations for dynamic lists
  • Performance: Using transform properties for hardware acceleration

Advanced Animation Patterns

SVG Path Animation

Animating SVG paths for drawing effects:

<script>
  import { Motion } from "svelte-motion";

  const pathData = "M 10 80 Q 95 10 180 80 T 350 80";

  const pathVariants = {
    initial: { 
      pathLength: 0, 
      opacity: 0,
      strokeDashoffset: 1000
    },
    animate: { 
      pathLength: 1, 
      opacity: 1,
      strokeDashoffset: 0,
      transition: {
        duration: 2,
        ease: "easeInOut"
      }
    }
  };
</script>

<svg width="400" height="200" class="border border-gray-300">
  <Motion
    variants={pathVariants}
    initial="initial"
    animate="animate"
    let:motion
    isSVG={true}
  >
    <path
      use:motion
      d={pathData}
      fill="none"
      stroke="blue"
      stroke-width="3"
      stroke-linecap="round"
    />
  </Motion>
</svg>
Enter fullscreen mode Exit fullscreen mode

Scroll-Triggered Animations

Animations that trigger based on scroll position:

<script>
  import { Motion } from "svelte-motion";
  import { onMount } from "svelte";

  let element;
  let isVisible = false;

  onMount(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        isVisible = entries[0].isIntersecting;
      },
      { threshold: 0.1 }
    );

    if (element) {
      observer.observe(element);
    }

    return () => {
      if (element) observer.unobserve(element);
    };
  });
</script>

<div bind:this={element}>
  <Motion
    initial={{ opacity: 0, y: 50 }}
    animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
    transition={{ duration: 0.6, ease: "easeOut" }}
    let:motion
  >
    <div 
      use:motion
      class="p-8 bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-lg"
    >
      <h2 class="text-2xl font-bold">Scroll to Animate</h2>
      <p>This content animates when it enters the viewport.</p>
    </div>
  </Motion>
</div>
Enter fullscreen mode Exit fullscreen mode

Complex Sequence Animations

Orchestrating multiple animations in sequence:

<script>
  import { Motion, AnimatePresence } from "svelte-motion";
  import { onMount } from "svelte";

  let step = 0;
  const steps = ['Step 1', 'Step 2', 'Step 3', 'Complete'];

  onMount(() => {
    const interval = setInterval(() => {
      step = (step + 1) % steps.length;
    }, 2000);
    return () => clearInterval(interval);
  });
</script>

<div class="p-8">
  <AnimatePresence mode="wait">
    {#key step}
      <Motion
        key={`step-${step}`}
        initial={{ opacity: 0, x: 50, rotate: -10 }}
        animate={{ opacity: 1, x: 0, rotate: 0 }}
        exit={{ opacity: 0, x: -50, rotate: 10 }}
        transition={{ 
          duration: 0.5,
          ease: "easeInOut"
        }}
        let:motion
      >
        <div 
          use:motion
          class="p-6 bg-blue-500 text-white rounded-lg text-center"
        >
          <h3 class="text-2xl font-bold">{steps[step]}</h3>
        </div>
      </Motion>
    {/key}
  </AnimatePresence>
</div>
Enter fullscreen mode Exit fullscreen mode

Common Issues / Troubleshooting

Issue 1: Animations Not Playing

Problem: Motion components render but animations don't execute.

Solution: Ensure you're using the use:motion directive on the target element:

<!-- ❌ Wrong - missing use:motion -->
<Motion animate={{ opacity: 1 }} let:motion>
  <div>Content</div>
</Motion>

<!-- ✅ Correct - using use:motion -->
<Motion animate={{ opacity: 1 }} let:motion>
  <div use:motion>Content</div>
</Motion>
Enter fullscreen mode Exit fullscreen mode

Issue 2: Exit Animations Not Working

Problem: Elements disappear instantly without exit animation.

Solution: Wrap conditional rendering with AnimatePresence:

<!-- ❌ Wrong - no AnimatePresence -->
{#if visible}
  <Motion exit={{ opacity: 0 }} let:motion>
    <div use:motion>Content</div>
  </Motion>
{/if}

<!-- ✅ Correct - with AnimatePresence -->
<AnimatePresence>
  {#if visible}
    <Motion exit={{ opacity: 0 }} let:motion>
      <div use:motion>Content</div>
    </Motion>
  {/if}
</AnimatePresence>
Enter fullscreen mode Exit fullscreen mode

Issue 3: SVG Animations Not Working

Problem: SVG properties don't animate correctly.

Solution: Use isSVG={true} prop on Motion component for SVG elements:

<!-- ✅ Correct - isSVG prop for SVG elements -->
<Motion animate={{ pathLength: 1 }} isSVG={true} let:motion>
  <path use:motion d="M10 10 L100 100" />
</Motion>
Enter fullscreen mode Exit fullscreen mode

Issue 4: Performance Issues with Many Animations

Problem: Animations are janky or cause performance problems.

Solution:

  • Use transform and opacity properties (hardware-accelerated)
  • Avoid animating width, height, top, left directly
  • Use will-change CSS property for elements that will animate
  • Debounce rapid state changes
<!-- ✅ Good - animating transform -->
<Motion animate={{ x: 100, scale: 1.2 }} let:motion>
  <div use:motion>Content</div>
</Motion>

<!-- ❌ Avoid - animating layout properties -->
<Motion animate={{ width: 200, height: 200 }} let:motion>
  <div use:motion>Content</div>
</Motion>
Enter fullscreen mode Exit fullscreen mode

Next Steps

Now that you've learned advanced animation techniques with svelte-animations, here are some directions to continue learning:

  1. Custom Animation Hooks: Create reusable animation utilities and composables
  2. Gesture Animations: Integrate with gesture libraries for drag, pinch, and swipe animations
  3. Layout Animations: Animate layout changes using FLIP technique
  4. Physics-Based Animations: Explore spring and physics-based motion
  5. Animation Performance: Learn about optimizing animations for 60fps
  6. Accessibility: Ensure animations respect prefers-reduced-motion
  7. Animation Testing: Learn how to test animated components
  8. Official Documentation: Visit the svelte-animations repository and svelte-motion documentation for complete API reference

Continue with other articles in this series to learn about specific components, animation patterns, and integration techniques.

Summary

You've learned how to implement advanced animation techniques with svelte-animations in Svelte, including animation orchestration, custom variants, complex transitions, SVG animations, and performance optimization. You should now be able to create sophisticated, performant animations that enhance user experience while maintaining smooth 60fps performance.

Top comments (0)