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
configurationtransformPagePoint
layoutId
(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)onTap
onTapCancel
MotionContainer
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