Skip to content

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.

License

Notifications You must be signed in to change notification settings

humanspeak/svelte-motion

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@humanspeak/svelte-motion

NPM version Build Status Coverage Status License Downloads CodeQL Install size Code Style: Trunk TypeScript Types Maintenance

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 Reach - 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 REPL
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
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.

Animation lifecycle

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

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.

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`} />

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

About

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.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Contributors 2

  •  
  •