svelte-motion Svelte Themes

Svelte Motion

An API-compatible Svelte wrapper for Framer Motion’s React API — bring Motion components, variants, transitions, gestures, and layout animations to Svelte via a thin compatibility layer.

@humanspeak/svelte-motion

Why are we here?

Motion vibes, Svelte runes. This brings Motion’s declarative animation goodness to Svelte with motion.<tag> components, interaction props, and composable config. If you spot a cool React example, drop it in an issue—we’ll port it. 😍

Requests welcome: Have a feature/prop/example you want? Please open an issue (ideally include a working Motion/React snippet or example link) and we’ll prioritize it.

Supported Elements

All standard HTML and SVG elements are supported as motion components (e.g., motion.div, motion.button, motion.svg, motion.circle). The full set is generated from canonical lists using html-tags, html-void-elements, and svg-tags, and exported from src/lib/html/.

  • HTML components respect void elements for documentation and generation purposes.
  • SVG components are treated as non-void.
  • Dashed tag names are exported as PascalCase components (e.g., color-profileColorProfile).

Configuration

MotionConfig

This package includes support for MotionConfig, which allows you to set default motion settings for all child components. See the React - Motion Config for more details.

<MotionConfig transition={{ duration: 0.5 }}>
    <!-- All motion components inside will inherit these settings -->
    <motion.div animate={{ scale: 1.2 }}>Inherits 0.5s duration</motion.div>
</MotionConfig>

Layout Animations (FLIP)

Svelte Motion supports minimal layout animations via FLIP using the layout prop:

<motion.div layout transition={{ duration: 0.25 }} />
  • layout: smoothly animates translation and scale between layout changes (size and position).
  • layout="position": only animates translation (no scale).

Exit Animations (AnimatePresence)

Animate elements as they leave the DOM using AnimatePresence. This mirrors Motion’s React API and docs for exit animations (reference).

<script lang="ts">
    import { motion, AnimatePresence } from '$lib'
    let show = $state(true)
</script>

<AnimatePresence>
    {#if show}
        <motion.div
            initial={{ opacity: 0, scale: 0.9 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.9 }}
            transition={{ duration: 0.5 }}
            class="size-24 rounded-md bg-cyan-400"
        />
    {/if}
</AnimatePresence>

<motion.button whileTap={{ scale: 0.97 }} onclick={() => (show = !show)}>Toggle</motion.button>
  • The exit animation is driven by exit and will play when the element unmounts.
  • Transition precedence (merged before running exit):
    • exit.transition (highest precedence)
    • component transition (merged with MotionConfig)
    • fallback default { duration: 0.35 }

Current Limitations

Some Motion features are not yet implemented:

  • reducedMotion settings
  • features configuration
  • Performance optimizations like transformPagePoint
  • Advanced transition controls
  • Shared layout / layoutId (planned)

External Dependencies

This package carefully selects its dependencies to provide a robust and maintainable solution:

Core Dependencies

  • motion
    • High-performance animation library for the web
    • Provides smooth, hardware-accelerated animations
    • Supports spring physics and custom easing
    • Used for creating fluid motion and transitions

Examples

Motion Demo / Route Live Demo
React - Enter Animation /tests/motion/enter-animation View Example
HTML Content (0→100 counter) /tests/motion/html-content View Example
Aspect Ratio /tests/motion/aspect-ratio View Example
Hover + Tap (whileHover + whileTap) /tests/motion/hover-and-tap View Example
Focus (whileFocus) /tests/motion/while-focus View Example
Rotate /tests/motion/rotate View Example
Random - Shiny Button by @verse_ /tests/random/shiny-button View Example
Fancy Like Button /tests/random/fancy-like-button View Example
Keyframes (square → circle → square; scale 1→2→1) /tests/motion/keyframes View Example
Animated Border Gradient (conic-gradient rotate) /tests/random/animated-border-gradient View Example
Exit Animation /tests/motion/animate-presence View Example

Interactions

Hover

Svelte Motion now supports hover interactions via the whileHover prop, similar to React Motion/Framer Motion.

<motion.div whileHover={{ scale: 1.05 }} />
  • whileHover accepts a keyframes object. It also supports a nested transition to override the global transition for hover only:
<motion.button
    whileHover={{ scale: 1.05, transition: { duration: 0.12 } }}
    transition={{ duration: 0.25 }}
/>
  • Baseline restoration: when the pointer leaves, changed values are restored to their pre-hover baseline. Baseline is computed from animate values if present, otherwise initial, otherwise sensible defaults (e.g., scale: 1, x/y: 0) or current computed style where applicable.
  • True-hover gating: hover behavior runs only on devices that support real hover and fine pointers (media queries (hover: hover) and (pointer: fine)), avoiding sticky hover states on touch devices.

Tap

<motion.button whileTap={{ scale: 0.95 }} />
  • Callbacks: onTapStart, onTap, onTapCancel are supported.
  • Accessibility: Elements with whileTap are keyboard-accessible (Enter and Space).
    • Enter or Space down → fires onTapStart and applies whileTap (Space prevents default scrolling)
    • Enter or Space up → fires onTap
    • Blur while key is held → fires onTapCancel
    • MotionContainer sets tabindex="0" automatically when whileTap is present and no tabindex/tabIndex is provided.

Focus

<motion.button whileFocus={{ scale: 1.05, outline: '2px solid blue' }} />
  • Animates when the element receives keyboard focus and restores baseline on blur.
  • Callbacks: onFocusStart, onFocusEnd are supported.
  • Perfect for keyboard navigation and accessibility enhancements.

Animation lifecycle

<motion.div
    onAnimationStart={(def) => {
        /* ... */
    }}
    onAnimationComplete={(def) => {
        /* ... */
    }}
/>

Variants

Variants allow you to define named animation states that can be referenced throughout your component tree. They're perfect for creating reusable animations and orchestrating complex sequences.

Basic usage

Instead of defining animation objects inline, create a Variants object with named states:

<script lang="ts">
    import { motion, type Variants } from '@humanspeak/svelte-motion'

    let isOpen = $state(false)

    const variants: Variants = {
        open: { opacity: 1, scale: 1 },
        closed: { opacity: 0, scale: 0.8 }
    }
</script>

<motion.div {variants} initial="closed" animate={isOpen ? 'open' : 'closed'}>Click me</motion.div>

Variant propagation

One of the most powerful features is automatic propagation through component trees. When a parent changes its animation state, all children with variants defined automatically inherit that state:

<script lang="ts">
    let isVisible = $state(false)

    const containerVariants: Variants = {
        visible: { opacity: 1 },
        hidden: { opacity: 0 }
    }

    const itemVariants: Variants = {
        visible: { opacity: 1, x: 0 },
        hidden: { opacity: 0, x: -20 }
    }
</script>

<motion.ul variants={containerVariants} initial="hidden" animate={isVisible ? 'visible' : 'hidden'}>
    <!-- Children automatically inherit parent's variant state -->
    <motion.li variants={itemVariants}>Item 1</motion.li>
    <motion.li variants={itemVariants}>Item 2</motion.li>
    <motion.li variants={itemVariants}>Item 3</motion.li>
</motion.ul>

How it works:

  • Parent sets animate="visible"
  • Children with variants automatically inherit "visible" state
  • Each child resolves its own variant definition
  • No need to pass animate props to children!

Staggered animations

Create staggered animations with transition delays:

{#each items as item, i}
    <motion.div variants={itemVariants} transition={{ delay: i * 0.1 }}>
        {item}
    </motion.div>
{/each}

See the Variants documentation for complete details and examples.

Server-side rendering

Motion components render their initial state during SSR. The container merges inline style with the first values from initial (or the first keyframes from animate when initial is empty) so the server HTML matches the starting appearance. On hydration, components promote to a ready state and animate without flicker.

<motion.div
    initial={{ opacity: 0, borderRadius: '12px' }}
    animate={{ opacity: 1 }}
    style="width: 100px; height: 50px"
/>

Notes:

  • Transform properties like scale/rotate are composed into a single transform style during SSR.
  • When initial is empty, the first keyframe from animate is used to seed SSR styles.
  • initial can be false to not run on initial

Utilities

useTime(id?)

  • Returns a Svelte readable store that updates once per animation frame with elapsed milliseconds since creation.
  • If you pass an id, calls with the same id return a shared timeline (kept in sync across components).
  • SSR-safe: Returns a static 0 store when window is not available.
<script lang="ts">
    import { motion, useTime } from '$lib'
    import { derived } from 'svelte/store'

    const time = useTime('global') // shared
    const rotate = derived(time, (t) => ((t % 4000) / 4000) * 360)
</script>

<motion.div style={`rotate: ${$rotate}deg`} />

useAnimationFrame(callback)

  • Runs a callback on every animation frame with the current timestamp.
  • The callback receives a DOMHighResTimeStamp representing the time elapsed since the time origin.
  • Returns a cleanup function that stops the animation loop.
  • Best used inside a $effect to ensure proper cleanup when the component unmounts.
  • SSR-safe: Does nothing and returns a no-op cleanup function when window is unavailable.
<script lang="ts">
    import { useAnimationFrame } from '$lib'

    let cubeRef: HTMLDivElement

    $effect(() => {
        return useAnimationFrame((t) => {
            if (!cubeRef) return

            const rotate = Math.sin(t / 10000) * 200
            const y = (1 + Math.sin(t / 1000)) * -50
            cubeRef.style.transform = `translateY(${y}px) rotateX(${rotate}deg) rotateY(${rotate}deg)`
        })
    })
</script>

<div bind:this={cubeRef}>Animated content</div>
  • Reference: Motion useAnimationFrame docs motion.dev.

useSpring

useSpring creates a readable store that animates to its latest target with a spring. You can either control it directly with set/jump, or have it follow another readable (like a time-derived value).

<script lang="ts">
    import { useTime, useTransform, useSpring } from '$lib'

    // Track another readable
    const time = useTime()
    const blurTarget = useTransform(() => {
        const phase = ($time % 2000) / 2000
        return 4 * (0.5 + 0.5 * Math.sin(phase * Math.PI * 2)) // 0..4
    }, [time])
    const blur = useSpring(blurTarget, { stiffness: 300 })

    // Or direct control
    const x = useSpring(0, { stiffness: 300 })
    // x.set(100) // animates to 100
    // x.jump(0)  // jumps without animation
</script>

<div style={`filter: blur(${$blur}px)`} />
  • Accepts number or unit string (e.g., "100vh") or a readable source.
  • Returns a readable with { set, jump } methods when used in the browser; SSR-safe on the server.
  • Reference: Motion useSpring docs motion.dev.

useTransform

useTransform creates a derived readable. It supports:

  • Range mapping: map a numeric source across input/output ranges with optional { clamp, ease, mixer }.
  • Function form: compute from one or more dependencies.

Range mapping example:

<script lang="ts">
    import { useTime, useTransform } from '$lib'
    const time = useTime()
    // Map 0..4000ms to 0..360deg, unclamped to allow wrap-around
    const rotate = useTransform(time, [0, 4000], [0, 360], { clamp: false })
</script>

<div style={`rotate: ${$rotate}deg`} />

Function form example:

<script lang="ts">
    import { useTransform } from '$lib'
    // Given stores a and b, compute their sum
    const add = (a: number, b: number) => a + b
    // deps are stores; body can access them via $ syntax
    const total = useTransform(() => add($a, $b), [a, b])
</script>

<span>{$total}</span>

Access the underlying element (bind:ref)

You can bind a ref to access the underlying DOM element rendered by a motion component:

<script lang="ts">
    import { motion } from '$lib'
    let el: HTMLDivElement | null = null
</script>

<motion.div bind:ref={el} animate={{ scale: 1.1 }} />

{#if el}
    <!-- use el for measurements, focus, etc. -->
{/if}

License

MIT © Humanspeak, Inc.

Credits

Made with ❤️ by Humanspeak

Top categories

Loading Svelte Themes