motion-icon
Animate any SVG icon on a spring. Load it with src from Lucide, Tabler, Heroicons, Material Symbols, Font Awesome or your own folder,
or put an <svg> inside. Stroke icons draw their lines in, and every icon can pop, bounce, rotate, wiggle, pulse or slide in any direction.
Combine both with animation="draw wiggle". It plays on hover, click, scroll or page load, or in a loop, and inside a button
or link it reacts to the whole button.
Preview
Hover an icon to draw it in.
Quick start
Save this as index.html and open it in a browser. No install, no build step.
<!doctype html>
<html>
<head>
<script type="module" src="https://cdn.jsdelivr.net/npm/motion-components@1/+esm"></script>
</head>
<body>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/heart.svg"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/bell.svg" animation="wiggle"></motion-icon>
</body>
</html>In a project with a bundler, install the package and import the component once:
npm install motion-componentsimport 'motion-components/motion-icon'Add an icon
There are four ways to give motion-icon its icon. Pick the one that fits your project.
1. From a CDN
Set src to the URL of an SVG file. Each URL is loaded once and shared by every icon that uses it.
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/heart.svg"></motion-icon>2. From your own folder
Put the SVG files in a folder of your site and point src at them. This works for any icon: one from an icon set, or one you drew
yourself. These two icons load from this site's own /icons folder:
my-site/
├── index.html
└── icons/
├── heart.svg
└── brackets.svg<motion-icon src="icons/heart.svg"></motion-icon>
<motion-icon src="icons/brackets.svg"></motion-icon>
A path without a leading slash, such as
3. Inline SVG
Paste the icon's <svg> inside the element. Most icon websites have a "Copy SVG" button.
<motion-icon>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5" />
</svg>
</motion-icon>4. From an npm package
With a bundler, import the icon as a string and set the icon property. The icon is part of your bundle, so nothing is loaded
at runtime.
<motion-icon id="like-icon"></motion-icon>import 'motion-components/motion-icon'
import { Heart } from 'lucide-static'
document.querySelector('#like-icon').icon = HeartVite's ?raw suffix imports an SVG file as a string. This works for any icon package that ships SVG files.
import 'motion-components/motion-icon'
import heart from '@tabler/icons/outline/heart.svg?raw'
document.querySelector('#like-icon').icon = heartIcon libraries
Every library below publishes its icons as SVG files on the jsDelivr CDN. Find an icon on the library's website, copy its line below and replace the name at the end of the URL. Hover the hearts to compare them.
<!-- Lucide -->
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/heart.svg"></motion-icon>
<!-- Tabler -->
<motion-icon src="https://cdn.jsdelivr.net/npm/@tabler/icons@3/icons/outline/heart.svg"></motion-icon>
<!-- Heroicons -->
<motion-icon src="https://cdn.jsdelivr.net/npm/heroicons@2/24/outline/heart.svg"></motion-icon>
<!-- Iconoir -->
<motion-icon src="https://cdn.jsdelivr.net/npm/iconoir@7/icons/regular/heart.svg"></motion-icon>
<!-- Feather -->
<motion-icon src="https://cdn.jsdelivr.net/npm/feather-icons@4/dist/icons/heart.svg"></motion-icon>
<!-- Phosphor -->
<motion-icon animation="pop" src="https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/regular/heart.svg"></motion-icon>
<!-- Bootstrap Icons -->
<motion-icon animation="pop" src="https://cdn.jsdelivr.net/npm/bootstrap-icons@1/icons/heart.svg"></motion-icon>
<!-- Material Symbols -->
<motion-icon animation="pop" src="https://cdn.jsdelivr.net/npm/@material-symbols/svg-400@0/outlined/favorite.svg"></motion-icon>
<!-- Font Awesome Free -->
<motion-icon animation="pop" src="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7/svgs/regular/heart.svg"></motion-icon>
<!-- Simple Icons (brands) -->
<motion-icon animation="pop" src="https://cdn.jsdelivr.net/npm/simple-icons@15/icons/github.svg"></motion-icon>| Library | URL | | License |
|---|---|---|---|
| Lucide | | Yes | ISC |
| Tabler | | Yes | MIT |
| Heroicons | | Yes | MIT |
| Iconoir | | Yes | MIT |
| Feather | | Yes | MIT |
| Phosphor | | Pops instead | MIT |
| Bootstrap Icons | | Pops instead | MIT |
| Material Symbols | | Pops instead | Apache-2.0 |
| Font Awesome Free | | Pops instead | CC BY 4.0 |
| Simple Icons (brands) | | Pops instead | CC0 |
draw traces the icon's strokes, so it needs an outline (stroke) icon. Filled icons pop instead. Font Awesome Free icons are
CC BY 4.0, which asks you to credit Font Awesome.
Frameworks
The same element works in every framework. In React and Vue you can also wrap the library's own icon component.
Load the library once, then use the tag anywhere.
<script type="module" src="https://cdn.jsdelivr.net/npm/motion-components@1/dist/motion-icon.js/+esm"></script>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/heart.svg"></motion-icon>React 19 passes attributes to custom elements. Use src, or put any icon component that renders an <svg> inside, such as lucide-react.
import 'motion-components/motion-icon'
import { Heart } from 'lucide-react'
export function LikeButton() {
return (
<button>
<motion-icon animation="pop">
<Heart />
</motion-icon>
Like
</button>
)
}Tell Vue that motion-* tags are custom elements (see Getting started), then use src or an icon component such as lucide-vue-next.
<script setup>
import 'motion-components/motion-icon'
import { Heart } from 'lucide-vue-next'
</script>
<template>
<button>
<motion-icon animation="pop">
<Heart />
</motion-icon>
Like
</button>
</template>Inside a button
Inside a <button> or <a>, the icon animates when the whole button is hovered or clicked, not only the icon.
<button>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/send.svg" animation="bounce"></motion-icon>
Send
</button>
<button>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/refresh-cw.svg" animation="rotate" trigger="click"></motion-icon>
Refresh
</button>
<a href="/next">
Next
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/arrow-right.svg" animation="slide-right"></motion-icon>
</a>More patterns, such as a notification bell, a download button and icon navigation, are on the Icon Recipes page.
Animations
Hover each icon. draw needs a stroke icon; the others work with any icon.
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/heart.svg" animation="draw"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/thumbs-up.svg" animation="pop"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/arrow-right.svg" animation="bounce"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/settings.svg" animation="rotate" duration="0.9"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/bell.svg" animation="wiggle"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/heart.svg" animation="pulse"></motion-icon>Slide
slide-up, slide-down, slide-left and slide-right move the icon a quarter of its size in
that direction. On hover the icon stays there while the pointer is over it and springs back on leave, which suits arrows and chevrons in
links. With the other triggers it moves out and springs back, like bounce.
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/arrow-up.svg" animation="slide-up"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/arrow-down.svg" animation="slide-down"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/arrow-left.svg" animation="slide-left"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/arrow-right.svg" animation="slide-right"></motion-icon>Draw and move together
Put draw in front of another animation to run both: the outline draws in, the fill fades in, and the icon moves at the same
time. On a filled icon, only the motion runs.
draw combines with any one motion, including the slides. With draw slide-right on hover, the arrow redraws as it
slides right, holds there, and slides back on leave without drawing again. Two motions don't combine: in animation="pop wiggle"
only the first motion, pop, runs.
<motion-icon
src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/zap.svg"
animation="draw wiggle"
style="--mc-icon-color: #eab308; --mc-icon-fill: #eab3084d"
></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/arrow-right.svg" animation="draw slide-right"></motion-icon>Triggers
trigger decides when the animation runs: hover (default), click, view (when it scrolls into view), mount or loop.
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/rocket.svg" trigger="view"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/mail.svg" trigger="click" animation="pop"></motion-icon>
<motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/bell.svg" trigger="loop" animation="wiggle" interval="1"></motion-icon>Color and size
The icon uses currentColor, so it follows the text color. Set --mc-icon-color to color it and --mc-icon-size to size it.
<motion-icon
src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/heart.svg"
style="--mc-icon-color: #e11d48; --mc-icon-size: 3rem"
></motion-icon>Fill and background
--mc-icon-fill colors the inside of an outline icon. With draw, the fill fades in as the lines finish. For an icon on a
colored background, style the element itself and make the icon white.
<!-- tinted fill: the color plus 33 for 20% opacity -->
<motion-icon
src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/star.svg"
animation="pop"
style="--mc-icon-color: #f59e0b; --mc-icon-fill: #f59e0b4d"
></motion-icon>
<!-- solid fill -->
<motion-icon
src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/heart.svg"
style="--mc-icon-color: #e11d48; --mc-icon-fill: #e11d48"
></motion-icon>
<!-- colored background -->
<motion-icon
src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/rocket.svg"
animation="bounce"
style="--mc-icon-color: #fff; background: #6366f1; padding: 0.6rem; border-radius: 14px"
></motion-icon>Properties
| Attribute | Property | Type | Default | Description |
|---|---|---|---|---|
| | | string | '' | URL of an SVG file. Each URL is loaded once and cached |
| | | string | '' | SVG markup to render. Takes precedence over |
| | | string | 'draw' | |
| | | string | 'hover' | |
| | | number | 0.6 | Duration of one run, in seconds |
| | | number | 0.4 | Spring bounciness as the icon settles (0 = no overshoot). |
| | | number | 0 | Seconds to wait before the animation starts |
| | | number | 1.2 | Seconds between runs with |
| | | boolean | true | With |
| | | string | '' | Accessible name. Without it the icon is decorative |
CSS custom properties
| Property | Type | Default | Description |
|---|---|---|---|
| | color | currentColor | Icon color |
| | color | none | Fills the inside of an outline icon |
| | length | 1.5em | Width and height of the icon |
Events
| Event | Type | Default | Description |
|---|---|---|---|
| | Event | An animation run starts | |
| | Event | An animation run finishes | |
| | Event | A run is stopped with | |
| | Event | The |
JavaScript API
replay() restarts the animation. Like every animated component, it also has play(), pause(), finish(), cancel() and playState. See the JavaScript API.
Security
Icons from src and icon are cleaned before they render. Only drawing elements such as <path> and
<circle> are kept. Scripts, styles, images, links, event handlers and references to other files are removed.
A src on another domain must allow cross-origin requests; jsDelivr and most CDNs do.
Accessibility
Icons are decorative by default and hidden from screen readers. Add label when the icon carries meaning on its own; it then gets
role="img" and that name. Inside a button with text, leave label empty. With prefers-reduced-motion,
the icon shows its final state with no animation.
Demo icons are loaded from the icon libraries listed above, under their own licenses.