Tree Shaking
The motion object (motion.div, motion.span, etc.) provides a convenient API that mirrors Framer Motion, but it bundles all 170+ HTML and SVG element wrappers regardless of how many you actually use. For production apps where bundle size matters, svelte-motion offers three ways to import only what you need.
The problem
When you use the motion object, bundlers cannot determine which properties are accessed at build time:
<script>
import { motion } from '@humanspeak/svelte-motion'
</script>
<!-- Only uses 2 elements, but all 170+ wrappers are bundled -->
<motion.div animate={{ opacity: 1 }}>
<motion.button whileTap={{ scale: 0.95 }}>Click me</motion.button>
</motion.div><script>
import { motion } from '@humanspeak/svelte-motion'
</script>
<!-- Only uses 2 elements, but all 170+ wrappers are bundled -->
<motion.div animate={{ opacity: 1 }}>
<motion.button whileTap={{ scale: 0.95 }}>Click me</motion.button>
</motion.div>This is because JavaScript object property access is a runtime operation that static analysis cannot optimize.
Option 1: Named exports
The simplest approach. Import components by name and they tree-shake automatically with any bundler:
<script>
import { MotionDiv, MotionButton } from '@humanspeak/svelte-motion'
</script>
<MotionDiv animate={{ opacity: 1 }}>
<MotionButton whileTap={{ scale: 0.95 }}>Click me</MotionButton>
</MotionDiv><script>
import { MotionDiv, MotionButton } from '@humanspeak/svelte-motion'
</script>
<MotionDiv animate={{ opacity: 1 }}>
<MotionButton whileTap={{ scale: 0.95 }}>Click me</MotionButton>
</MotionDiv>Every HTML and SVG element has a corresponding Motion-prefixed export. The naming convention is Motion + the PascalCase element name:
| Element | Named export |
|---|---|
div | MotionDiv |
button | MotionButton |
span | MotionSpan |
a | MotionA |
svg | MotionSvg |
circle | MotionCircle |
path | MotionPath |
h1…h6 | MotionH1…MotionH6 |
input | MotionInput |
img | MotionImg |
All other exports (animate, useSpring, AnimatePresence, types, etc.) continue to work alongside named component imports:
<script>
import { MotionDiv, AnimatePresence, type Variants } from '@humanspeak/svelte-motion'
</script><script>
import { MotionDiv, AnimatePresence, type Variants } from '@humanspeak/svelte-motion'
</script>Option 2: Vite plugin
If you prefer the motion.div syntax, the svelteMotionOptimize Vite plugin automatically rewrites it into direct imports at build time. Your source code stays unchanged:
<!-- You write this: -->
<script>
import { motion } from '@humanspeak/svelte-motion'
</script>
<motion.div animate={{ opacity: 1 }}>Hello</motion.div>
<!-- The plugin transforms it to: -->
<script>
import SvelteMotionDiv from '@humanspeak/svelte-motion/html/Div.svelte'
</script>
<SvelteMotionDiv animate={{ opacity: 1 }}>Hello</SvelteMotionDiv><!-- You write this: -->
<script>
import { motion } from '@humanspeak/svelte-motion'
</script>
<motion.div animate={{ opacity: 1 }}>Hello</motion.div>
<!-- The plugin transforms it to: -->
<script>
import SvelteMotionDiv from '@humanspeak/svelte-motion/html/Div.svelte'
</script>
<SvelteMotionDiv animate={{ opacity: 1 }}>Hello</SvelteMotionDiv>Setup
Add the plugin to your vite.config.ts before the SvelteKit plugin:
import { svelteMotionOptimize } from '@humanspeak/svelte-motion/vite'
import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
svelteMotionOptimize(),
sveltekit()
]
})import { svelteMotionOptimize } from '@humanspeak/svelte-motion/vite'
import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
svelteMotionOptimize(),
sveltekit()
]
})No changes to your .svelte files are needed. The plugin handles:
- Opening and closing tags:
<motion.div>…</motion.div> - Self-closing tags:
<motion.hr /> - Script references:
const C = motion.div - Mixed imports: preserves other named imports like
animate,useSpring - SVG elements:
motion.circle,motion.path, etc.
Option 3: Direct imports
For maximum control, import components directly from their .svelte files:
<script>
import Div from '@humanspeak/svelte-motion/html/Div.svelte'
import Button from '@humanspeak/svelte-motion/html/Button.svelte'
</script>
<Div animate={{ opacity: 1 }}>
<Button whileTap={{ scale: 0.95 }}>Click me</Button>
</Div><script>
import Div from '@humanspeak/svelte-motion/html/Div.svelte'
import Button from '@humanspeak/svelte-motion/html/Button.svelte'
</script>
<Div animate={{ opacity: 1 }}>
<Button whileTap={{ scale: 0.95 }}>Click me</Button>
</Div>This bypasses the package entry point entirely and works with any bundler.
Which approach to choose
| Named exports | Vite plugin | Direct imports | |
|---|---|---|---|
| Setup | None | One line in vite.config | None |
| Syntax | <MotionDiv> | <motion.div> | <Div> |
| Bundler support | All | Vite only | All |
| Tree-shakes | Yes | Yes | Yes |
| Autocomplete | Full | Full | Per-import |
Recommended: Use named exports for new projects. They work everywhere, require no configuration, and provide full autocomplete. Use the Vite plugin if you’re migrating an existing codebase and want to keep the motion.div syntax.
Related
- Get started — Motion component overview
- AnimatePresence — Exit animations
- Variants — Named animation states