npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@respeak/lucide-motion-vue

v0.6.2

Published

523 Lucide icons / 815 named animations for Vue 3 — ergonomic animateOnHover/Tap/View triggers, a composable <AnimateIcon> wrapper, and a Nuxt module. Tree-shakable, SSR-safe.

Readme

@respeak/lucide-motion-vue

npm downloads bundle size Vue 3 types live demo

The largest animated icon library for Vue 3. 523 Lucide icons across 815 named animation variants — drop-in and tree-shakeable, with a live gallery, variant switcher, and copy-paste snippets.

Animated icon preview

▶︎ Live gallery + docs — hover any icon to preview; click for variants, props, and copy-paste snippets. Built on Motion for Vue, ships a Nuxt module, SSR-safe, fully tree-shakable per icon.

  • 523 icons / 815 named variants, tree-shakable per chunk — one icon, one bundle entry
  • Multiple animations per icon. <Heart animation="fill" />, <Sun animation="alt" />, <Link2 animation="apart" />. Variants can carry materially different motion and different element graphs under one component name (e.g. Sun's default is the animate-ui sunburst, alt is the lucide-animated minimal silhouette — same <Sun> import).
  • Ergonomic triggers: animateOnHover, animateOnTap, animateOnView
  • Composable <AnimateIcon> wrapper drives nested icons via provide/inject
  • Bind triggers to an ancestor with triggerTarget="parent" / closest:button — no markup refactor
  • Nuxt module with auto-imports — <HeartAnimated /> works with no per-file imports
  • SSR-safe with hydration replay (no flash, no mismatch)
  • Full TypeScript types, native Motion loops, currentColor styling
  • Works standalone (<Heart animateOnHover />) or composed (<AnimateIcon> over anything)
  • Forge maintainer tool (see below) generates AI-designed icon animations for review — pnpm forge

Contents

Install

pnpm add @respeak/lucide-motion-vue motion-v

Peer deps: vue ^3.3, motion-v ^2.

Usage

<script setup lang="ts">
import {
  AnimateIcon,
  Heart,
  BetweenVerticalStart,
} from '@respeak/lucide-motion-vue'
</script>

<template>
  <!-- Standalone, self-wrapped -->
  <Heart animateOnHover />
  <BetweenVerticalStart animateOnTap />

  <!-- Named animation variant -->
  <Heart animateOnHover animation="fill" />

  <!-- Composed: one trigger, many children -->
  <AnimateIcon animateOnHover>
    <span>
      <BetweenVerticalStart />
      <Heart />
    </span>
  </AnimateIcon>

  <!-- Button-as-trigger (renderless, via scoped slot) -->
  <AnimateIcon animateOnHover as="template" v-slot="{ on }">
    <v-btn color="primary" v-on="on">
      <BetweenVerticalStart :size="20" class="mr-2" />
      Align columns
    </v-btn>
  </AnimateIcon>
</template>

Collision-safe names

If you use this library alongside lucide-vue-next (static icons), both export Heart. Use the *Animated alias to disambiguate:

import { Heart as StaticHeart } from 'lucide-vue-next'
import { HeartAnimated } from '@respeak/lucide-motion-vue'

Per-icon subpath imports

Every icon is also exposed under /icons/<kebab-name> for consumers who want guaranteed separate chunks (or whose bundler trips on large barrel files):

import Heart from '@respeak/lucide-motion-vue/icons/heart'
import BetweenVerticalStart from '@respeak/lucide-motion-vue/icons/between-vertical-start'

Tree-shaking works with either import style (package is ESM + "sideEffects": false).

Nuxt module

For Nuxt 3 apps the package ships a module at @respeak/lucide-motion-vue/nuxt that auto-registers <AnimateIcon> and every icon — no imports needed in your templates.

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@respeak/lucide-motion-vue/nuxt'],
})
<!-- any .vue file, no imports needed -->
<HeartAnimated animateOnHover />
<Link2Animated animateOnTap animation="unlink" />
<AnimateIcon animateOnHover as="template" v-slot="{ on }">
  <button v-on="on">
    <HeartAnimated :size="20" /> Favorite
  </button>
</AnimateIcon>

The default naming is suffixed (<HeartAnimated>, <StarAnimated>, …) so the module coexists with lucide-vue-next's static <Heart>, <Star> without collision — keep both installed and pick per-usage. Override the scheme in nuxt.config if you want something shorter:

export default defineNuxtConfig({
  modules: ['@respeak/lucide-motion-vue/nuxt'],
  lucideMotion: { prefix: 'M', suffix: '' },  // → <MHeart>, <MLink2>
})

Per-icon tree-shaking is preserved — templates that only reference <HeartAnimated> ship one chunk, not the whole library.

Props

Every icon accepts:

| prop | type | default | description | |-----------------------|--------------------------|----------|---------------------------------------------------------------| | size | number | 28 | Rendered width/height in px. | | strokeWidth | number | 2 | SVG stroke width. | | animate | boolean \| string | false | Programmatic trigger. Pass a variant name to select it. | | animateOnHover | boolean \| string | false | Play while hovered. | | animateOnTap | boolean \| string | false | Play while pointer is down. | | animateOnView | boolean \| string | false | Play when the icon enters the viewport. | | animation | string | default| Which named variant group to pull from (e.g. fill). | | persistOnAnimateEnd | boolean | false | Keep final state instead of returning to initial. | | initialOnAnimateEnd | boolean | false | Force snap to initial when animation ends. | | clip | boolean | false | Clip the icon's overflow at its bounding box — see below. | | triggerTarget | 'self' \| 'parent' \| \closest:${string}`|'self'` | Bind hover/tap to an ancestor — see Migrating existing buttons. |

Available animation names are icon-specific and mirror upstream animate-ui — e.g. Heart supports default and fill, BetweenVerticalStart supports default and default-loop, Link2 supports default/apart/unlink/link. See Discovering variants for a programmatic way to list them, or browse the docs site in docs/ (pnpm docs:dev).

A few animations deliberately move parts of the icon outside its viewBox — send's plane flies off before returning; rocket's launch variant lifts off and out. Those read correctly only when the overflow is hidden, so they disappear on exit instead of touring around the rest of the page. That's what clip is for:

<SendAnimated animateOnHover clip />
<Rocket animateOnView clip animation="launch" />

Off by default because other icons (e.g. link-2's burst particles) are designed to render outside their box and would break with clipping on.

Icons that are conceptually infinite (LoaderCircle, Loader, LoaderPinwheel, etc.) bake repeat: Infinity into their own variant transitions, so they loop as soon as you trigger them — no prop required. One-shot icons play once per trigger.

Styling

Icons use stroke="currentColor" (and fill: 'currentColor' for fill-based variants), so color is driven by the parent's CSS color — same pattern as lucide-vue-next. Fill-based animations automatically pick up whatever color you set, so the tween stays on-brand.

<!-- Any of these work, including fill animations -->
<Heart animateOnHover class="text-rose-500" />
<Heart animateOnHover animation="fill" style="color: #4f46e5" />
<div style="color: var(--my-brand)"><Heart animateOnHover /></div>

Width and height can come from the size prop or CSS. Utility classes (w-6 h-6, size-8), scoped styles, and inline style all land on the inner <svg> — whether the icon self-wraps (any trigger prop set) or not.

<Heart :size="40" />
<Heart animateOnHover class="w-10 h-10" />
<Heart animateOnHover style="width: 40px; height: 40px" />

transform must be inline (not via class)

motion-v writes an inline style="transform: …" on the rendered <svg> to drive its animations, and inline style beats any class-defined transform. So if you want to apply your own transform to a self-wrapped icon (typically translateY(-50%) for the icon-in-input centering idiom), pass it via inline style=, not via a CSS class — mergeProps flows the inline style through alongside motion-v's transform, and your translation is preserved.

<!-- ✅ works: inline-style transform reaches the svg -->
<div style="position: relative">
  <Search animateOnHover style="position: absolute; left: 10px; top: 50%;
    transform: translateY(-50%); width: 18px; height: 18px" />
  <input style="padding-left: 36px" />
</div>

<!-- ❌ broken: motion-v overwrites .input-icon's transform with `none` -->
<style>.input-icon { position: absolute; transform: translateY(-50%); }</style>
<Search animateOnHover class="input-icon" />

This only matters for transform — every other property (position, top, width, color, etc.) reaches the svg fine via class. And it doesn't affect flex/grid centering at all (e.g. Vuetify <v-text-field #prepend-inner> works with no CSS).

<AnimateIcon> wrapper

import { AnimateIcon } from '@respeak/lucide-motion-vue'

A renderless wrapper that catches trigger events on the slotted icon (or an ancestor) and propagates animation state (via provide/inject) to any icon nested inside. No DOM box of its own. Use it when one trigger should drive multiple icons, or when the trigger element should be something other than the icon itself (button, card, link…).

Props

All the trigger/animation props below also work directly on individual icons; the wrapper just lets you share them across a subtree.

| prop | type | default | description | |-----------------------|---------------------|-----------|------------------------------------------------------------| | animate | boolean \| string | false | Programmatic trigger. Pass a variant name to select it. | | animateOnHover | boolean \| string | false | Play while hovered. | | animateOnTap | boolean \| string | false | Play while pointer is down. | | animateOnView | boolean \| string | false | Play when the wrapper enters the viewport. | | animation | string | default | Which named variant group to pull from. | | persistOnAnimateEnd | boolean | false | Keep final state instead of returning to initial. | | initialOnAnimateEnd | boolean | false | Force snap to initial when animation ends. | | clip | boolean | false | Clip overflow at the icon's viewBox — for "exit" animations. | | as | 'default' \| 'template' | 'default' | Rendering mode — see below. | | triggerTarget | 'self' \| 'parent' \| \closest:${string}`|'self'` | Bind hover/tap to an ancestor instead of the icon — see below. |

Rendering modes

  • Default: no DOM wrapper. Pointer listeners and the animateOnView ref are forwarded onto the slot's first vnode (the icon's <svg>). Matches lucide-vue-next's bare-svg shape so CSS idioms like position: absolute overlays keep working.
  • as="template": renderless — exposes { on, viewRef } via the default scoped slot so you can bind them to any element (e.g. a <v-btn>, <a>, <button>, whole card). Nothing extra in the DOM.
<!-- Default mode: the first slotted svg becomes the trigger area -->
<AnimateIcon animateOnHover>
  <Heart :size="20" />
</AnimateIcon>

<!-- Template mode: the button is the trigger -->
<AnimateIcon animateOnHover as="template" v-slot="{ on }">
  <button v-on="on" class="card">
    <Heart :size="20" />
    <span>Favorite</span>
  </button>
</AnimateIcon>

Because default mode forwards events onto the slot's first vnode (not its own DOM box), driving multiple icons from one trigger needs a single wrapping element so the trigger area covers them all:

<AnimateIcon animateOnHover>
  <span style="display: inline-flex; gap: 12px">
    <Heart :size="20" />
    <Trash2 :size="20" />
  </span>
</AnimateIcon>

Listing siblings directly under <AnimateIcon> would only fire on the first one. Use as="template" if you want full control over which element captures events.

Migrating existing buttons (triggerTarget)

If you already have <button><Icon /></button> markup and want hover to fire on the whole button, set triggerTarget on the icon itself — no wrapper, no markup refactor:

<!-- Drop-in: the existing button stays untouched. -->
<button class="btn">
  <Heart animateOnHover triggerTarget="parent" :size="18" />
  Favorite
</button>

<!-- Extra wrappers between icon and button? Climb with closest. -->
<button class="btn">
  <span class="flex gap-2">
    <Trash2 animateOnHover triggerTarget="closest:button" :size="18" />
    Delete
  </span>
</button>

Which one to reach for:

  • triggerTarget="parent" / "closest:…" — best for migrations and single-icon buttons. Additive (two props), no markup change.
  • as="template" — best when one trigger should drive several icons, or when the trigger isn't an ancestor of the icon.

triggerTarget applies in as="default" mode only. In as="template" mode you already pick the trigger element by binding on.

Discovering variants (iconsMeta)

Every icon's kebab name, Pascal name, and full list of animation variants is exported as a plain array — handy for building custom pickers, auto-generated docs, or validation.

import { iconsMeta, type IconMeta } from '@respeak/lucide-motion-vue'

iconsMeta[0]
// → {
//     kebab: 'accessibility',
//     pascal: 'Accessibility',
//     animations: [{ name: 'default', source: 'animate-ui' }],
//   }

// Each variant carries its upstream `source` ('animate-ui' | 'lucide-animated'
// | 'hand-written') for attribution — see ATTRIBUTIONS.md.
iconsMeta.find(m => m.pascal === 'Heart')?.animations.map(a => a.name)
// → ['default', 'fill']

// Some icons ship variants with materially different element graphs
// (different path splits, different animated parts) — `Sun`, `AudioLines`,
// `Cast`, `MessageSquareMore` and a dozen others. The component name and
// the `iconsMeta` row stay the same; only the `animation` prop changes:
iconsMeta.find(m => m.pascal === 'Sun')?.animations
// → [{ name: 'default', source: 'animate-ui' }, { name: 'alt', source: 'lucide-animated' }]

TypeScript

First-class. The package ships .d.ts alongside every chunk:

  • Every icon has typed props (size, strokeWidth, animate, animateOnHover, …).
  • AnimateIcon's props are typed the same way; as is a literal union.
  • IconTriggerProps and IconMeta are exported for anyone building a wrapper or registry on top.
  • No any in the public surface.
import type { IconTriggerProps, IconMeta } from '@respeak/lucide-motion-vue'

Accessibility

Icons render as plain <svg> elements, so standard SVG a11y patterns apply:

  • Decorative (next to text that already says the thing): add aria-hidden="true".
  • Meaningful (icon-only button, status indicator): label it via aria-label on the button/parent, or wrap in a role="img" element with an accessible name.
<button aria-label="Favorite this item">
  <Heart aria-hidden="true" animateOnHover />
</button>

<span role="img" aria-label="Loading">
  <LoaderCircle />
</span>

Animation is purely visual — it never changes the DOM structure or any aria-* state, so screen readers aren't affected by it. Respect prefers-reduced-motion by wrapping in a parent that toggles the icon out when the user prefers reduced motion, or falls back to lucide-vue-next for those users.

Docs site

Live: https://respeak-io.github.io/lucide-motion-vue/

Two views:

  • Browse icons (/) — searchable grid, variant picker, copy-paste snippets.
  • Read the docs (/#/docs) — usage patterns with live demos: icons in buttons, variants, color, programmatic triggers, and a section for AI agents pointing at llms.txt.

To run locally:

pnpm docs:dev       # serve it on http://localhost:5174
pnpm docs:build     # emit static site to docs-dist/

Set VITE_DOCS_BASE=/repo-name/ when building for a GitHub Pages subpath deploy.

For AI agents

A concise machine-readable API reference is served at /llms.txt (and checked into docs/public/llms.txt). Point your agent's system prompt or repo rules at it — see the docs site's "For AI agents" section for a drop-in Cursor rule and prompt template.

The Forge — design new variants

Need an animation that doesn't ship out of the box? The repo includes a maintainer-grade Vite app that hands an icon to Claude (Sonnet 4.6 or Opus 4.7) and gets back 3 distinct animation proposals as strict JSON, side-by-side previewable, exportable as a hand-written SFC into src/icons/.

ANTHROPIC_API_KEY=… pnpm forge       # → http://localhost:5173

What it does:

  • Generate — pick a Lucide icon (or paste an SVG), optionally enable spawning auxiliary elements (sparks, ripples, motion trails) and silhouette morphing. The model returns 3 proposals titled like "Wand draws stars", each with a 1-2-sentence rationale.
  • Review — live preview each proposal in the same UI; tweak the source SVG and re-run; reject and re-prompt if none clear the bar.
  • Ship — export the chosen proposal as a hand-written SFC matching the library's standard shape. The generated file gets a // Hand-written sentinel so the codemod scripts skip it on re-run.

The full design-quality bar (multi-element motion, semantic mapping, phase variation, magnitude floors, what won't render) lives in forge/style-guide.md. It's hand-authored for LLM consumption — point your own agent workflow at it if you'd rather DIY.

Contributing / regenerating icons

The icons in src/icons/ are generated from two upstream registries:

node scripts/port-icons.mjs --force            # animate-ui variants
node scripts/port-pqoqubbw-icons.mjs --force   # lucide-animated / pqoqubbw variants

Each script clones its upstream into /tmp/…-upstream on first run (shallow), then for every icon:

  1. Extracts the module prelude (module-level constants, helper Variants, spring configs).
  2. Extracts the const animations = {…} block.
  3. Extracts the IconComponent return JSX and rewrites it to a Vue template.
  4. Emits src/icons/<kebab>.vue using the standard SFC shape.
  5. Regenerates src/index.ts with Name + NameAnimated exports.

When upstream adds a new icon, re-run the script — it auto-picks up new directories. Generated files are overwritten on re-run when --force is passed; without --force they're left alone.

Hand-written files (see below) are always preserved, even with --force, as long as their header comment contains the sentinel string Hand-written or Hand-ported — that's how the script tells a deliberate in-repo icon apart from an accidental edit to a generated file.

Adding a hand-written icon

Drop a .vue file in src/icons/ matching the pattern of the existing generated files (script + scoped slot + self-wrap branch + motion.svg with :variants= bindings). Include // Hand-written (or // Hand-ported) in the header comment so the port scripts know to skip it on re-run. pnpm build picks it up via the icon-entry glob in vite.config.ts, and re-running the codemod regenerates the barrel to include it.

If upstream later ships its own variant of an icon you've already written by hand (e.g. rocket gained a lucide-animated variant after the initial port), add it as an additional variant inside the same SFC and append a new row to the icon's animations array in src/icons-meta.ts with the correct source. Don't rename or remove the hand-written variants — cross-source icons are how this is supposed to look.

To do that splice with less guesswork, run:

node scripts/port-pqoqubbw-icons.mjs --augment=<kebab> [--variant-name=<name>]

It parses the upstream .tsx and your existing hand-written .vue, matches upstream paths to your pathN binding keys by comparing d attributes (fuzzy on the first 12 chars — small decimal tweaks survive), and prints a ready-to-paste variant block plus the icons-meta.ts row addition. The script never writes to the hand-written file — the paste is still a human decision. Default variant name is lucide-animated; override with --variant-name=... if that would collide with an existing key.

License & attributions

MIT for the framework code in src/core/.

Icon variants come from several upstream projects — each variant in iconsMeta carries a source tag for attribution:

Underlying SVG paths come from Lucide (ISC).

See ATTRIBUTIONS.md for the readable version and LICENSE for the full legal text.