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.
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/.
color-profile → ColorProfile).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>
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).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>
exit and will play when the element unmounts.exit.transition (highest precedence)transition (merged with MotionConfig){ duration: 0.35 }Some Motion features are not yet implemented:
reducedMotion settingsfeatures configurationtransformPagePointlayoutId (planned)This package carefully selects its dependencies to provide a robust and maintainable solution:
| 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 |
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 }}
/>
animate values if present, otherwise initial, otherwise sensible defaults (e.g., scale: 1, x/y: 0) or current computed style where applicable.(hover: hover) and (pointer: fine)), avoiding sticky hover states on touch devices.<motion.button whileTap={{ scale: 0.95 }} />
onTapStart, onTap, onTapCancel are supported.whileTap are keyboard-accessible (Enter and Space).onTapStart and applies whileTap (Space prevents default scrolling)onTaponTapCancelMotionContainer sets tabindex="0" automatically when whileTap is present and no tabindex/tabIndex is provided.<motion.button whileFocus={{ scale: 1.05, outline: '2px solid blue' }} />
onFocusStart, onFocusEnd are supported.<motion.div
onAnimationStart={(def) => {
/* ... */
}}
onAnimationComplete={(def) => {
/* ... */
}}
/>
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.
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>
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:
animate="visible"variants automatically inherit "visible" stateanimate props to children!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.
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:
scale/rotate are composed into a single transform style during SSR.initial is empty, the first keyframe from animate is used to seed SSR styles.initial can be false to not run on initialid, calls with the same id return a shared timeline (kept in sync across components).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`} />
DOMHighResTimeStamp representing the time elapsed since the time origin.$effect to ensure proper cleanup when the component unmounts.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>
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)`} />
"100vh") or a readable source.{ set, jump } methods when used in the browser; SSR-safe on the server.useTransform creates a derived readable. It supports:
{ clamp, ease, mixer }.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>
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}
MIT © Humanspeak, Inc.
Made with ❤️ by Humanspeak