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.

heart
bell
rocket
mail
circle-check
sparkles

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-components
import '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:

/icons/heart.svg
/icons/brackets.svg
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 icons/heart.svg, is relative to the page. For pages in subfolders, start the path with a slash, such as /icons/heart.svg, so it always points to the folder at the root of your site.

3. Inline SVG

Paste the icon's <svg> inside the element. Most icon websites have a "Copy SVG" button.

check
<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>

npm install lucide-static. Every icon is a named export, and only the ones you import end up in your bundle.

import 'motion-components/motion-icon'
import { Heart } from 'lucide-static'

document.querySelector('#like-icon').icon = Heart

Vite'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 = heart

Icon 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
Tabler
Heroicons
Iconoir
Feather
Phosphor
Bootstrap Icons
Material Symbols
Font Awesome Free
Simple Icons
<!-- 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 draw License
Lucide lucide-static@1/icons/{name}.svg Yes ISC
Tabler @tabler/icons@3/icons/outline/{name}.svg Yes MIT
Heroicons heroicons@2/24/outline/{name}.svg Yes MIT
Iconoir iconoir@7/icons/regular/{name}.svg Yes MIT
Feather feather-icons@4/dist/icons/{name}.svg Yes MIT
Phosphor @phosphor-icons/core@2/assets/regular/{name}.svg Pops instead MIT
Bootstrap Icons bootstrap-icons@1/icons/{name}.svg Pops instead MIT
Material Symbols @material-symbols/svg-400@0/outlined/{name}.svg Pops instead Apache-2.0
Font Awesome Free @fortawesome/fontawesome-free@7/svgs/regular/{name}.svg Pops instead CC BY 4.0
Simple Icons (brands) simple-icons@15/icons/{name}.svg 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.

Next
<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.

draw
pop
bounce
rotate
wiggle
pulse
<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.

slide-up
slide-down
slide-left
slide-right
<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.

draw wiggle
draw pulse
draw rotate
draw pop
draw slide-right
<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.

view
click
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.

#e11d48
#d946ef
#16a34a
<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
solid fill
background
<!-- 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
src src string '' URL of an SVG file. Each URL is loaded once and cached
icon icon string '' SVG markup to render. Takes precedence over src. When both are empty, the child <svg> is used
animation animation string 'draw' 'draw', 'pop', 'bounce', 'rotate', 'wiggle', 'pulse', 'slide-up', 'slide-down', 'slide-left' or 'slide-right', or draw plus one of the others, such as 'draw wiggle', to run both. draw needs a stroke icon and falls back to pop
trigger trigger string 'hover' 'hover', 'click', 'view', 'mount' or 'loop'. Inside a button or link, hover and click follow the button or link
duration duration number 0.6 Duration of one run, in seconds
bounce bounce number 0.4 Spring bounciness as the icon settles (0 = no overshoot). wiggle always uses at least 0.6
delay delay number 0 Seconds to wait before the animation starts
interval interval number 1.2 Seconds between runs with trigger="loop"
once once boolean true With trigger="view", animate only the first time. Set once="false" to replay on every entry
label label string '' Accessible name. Without it the icon is decorative

CSS custom properties

Property Type Default Description
--mc-icon-color color currentColor Icon color
--mc-icon-fill color none Fills the inside of an outline icon
--mc-icon-size length 1.5em Width and height of the icon

Events

Event Type Default Description
motion-start Event An animation run starts
motion-finish Event An animation run finishes
motion-cancel Event A run is stopped with cancel()
error Event The src URL could not be loaded

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.