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/
.
- 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-profile
→ColorProfile
).
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>
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>
- 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 withMotionConfig
) - fallback default
{ duration: 0.35 }
Some Motion features are not yet implemented:
reducedMotion
settingsfeatures
configuration- Performance optimizations like
transformPagePoint
- Advanced transition controls
- Shared layout /
layoutId
(planned)
This package carefully selects its dependencies to provide a robust and maintainable solution:
- 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
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 |
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 nestedtransition
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, otherwiseinitial
, 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.
<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 applieswhileTap
(Space prevents default scrolling) - Enter or Space up → fires
onTap
- Blur while key is held → fires
onTapCancel
MotionContainer
setstabindex="0"
automatically whenwhileTap
is present and notabindex
/tabIndex
is provided.
- Enter or Space down → fires
<motion.div
onAnimationStart={(def) => {
/* ... */
}}
onAnimationComplete={(def) => {
/* ... */
}}
/>
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 singletransform
style during SSR. - When
initial
is empty, the first keyframe fromanimate
is used to seed SSR styles.
- 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 whenwindow
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
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
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>
- Reference: Motion useTransform docs motion.dev.
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