PulsoIcon

React

pulsoicon-react — animated React icons with the same names and props as lucide-react.

Installation

npm install pulsoicon-react

React 18 or newer. Until 5.2.1 the package was called pulsoicon; from 6.0.1 it is pulsoicon-react and only the import changes.

Quick start

import { Bell, Calendar, Trash2 } from "pulsoicon-react";

export function Toolbar() {
  return (
    <>
      <button>
        <Bell size={20} /> Notifications
      </button>
      <button>
        <Calendar /> Schedule
      </button>
      <button>
        <Trash2 color="crimson" strokeWidth={1.5} /> Delete
      </button>
    </>
  );
}

Hover a button and its icon plays.

Props

Prop Type Default What it does
size number | string 24 Width and height
color string currentColor Stroke colour, and fill colour when solid
strokeWidth number | string 2 Stroke width on the 24-unit grid
absoluteStrokeWidth boolean false Keep the stroke the same number of pixels at any size
solid boolean false Filled icon instead of outline
animated boolean true false makes the icon fully static: nothing plays it, not even the ref or play
autoAnimated boolean true Play the built-in hover / focus trigger. false turns that off; the ref and play still work
play boolean true plays the motion, false returns to rest

autoAnimated and play only exist while the icon can animate, so TypeScript rejects <Bell animated={false} play />. A wrapper that drops a prop should use the exported DistributiveOmit<IconProps, "size"> instead of Omit, which would lose that check.

Any other SVG attribute (className, style, aria-label, …) is passed to the <svg>.

Animated outline

<Heart />

Animated solid

<Heart solid />

Static icons

<Heart animated={false} />
<Heart solid animated={false} />

animated={false} turns every trigger off: hover, focus, the ref and play.

Hover and focus

An icon plays when the pointer enters, or focus moves into, its closest button, a, label, summary, [role="button"], [role="link"], [role="tab"] or [role="menuitem"]. With none, it plays on its own hover. Any element becomes the trigger with data-icon-trigger:

<div className="card" data-icon-trigger>
  <MapPin /> 3 venues near you
</div>

Programmatic animation

Every icon forwards a ref with startAnimation() and stopAnimation(). Both return a promise that resolves when the motion has finished. Set autoAnimated={false} when only your code should decide:

import { useRef } from "react";
import { Check, type IconHandle } from "pulsoicon-react";

export function SaveButton() {
  const check = useRef<IconHandle>(null);

  async function handleSave() {
    await saveDocument(); // your own save
    await check.current?.startAnimation();
  }

  return (
    <button onClick={handleSave}>
      <Check ref={check} autoAnimated={false} />
      Save
    </button>
  );
}

State-driven animation

play starts the motion when it turns true and returns to rest when it turns false:

<Bell autoAnimated={false} play={hasNewMessage} />

Customisation

Each icon renders <svg class="pulsoicon pulsoicon-<name>">, so CSS can style every icon or one:

.pulsoicon {
  vertical-align: -0.125em;
}

.pulsoicon-bell {
  color: #f2552c;
}

Migration from Lucide

Component names and props match lucide-react; usually only the import changes:

- import { Bell, Calendar, Trash2 } from "lucide-react";
+ import { Bell, Calendar, Trash2 } from "pulsoicon-react";

Lucide’s helpers (createLucideIcon, LucideProvider, the icons object) are not included. More in Migrating.

Tree shaking

Every icon is its own module and the package is side-effect free, so a bundler keeps only the icons you import. Import icons by name from pulsoicon-react, or one by path:

import { Calendar } from "pulsoicon-react/icons/calendar";

Dynamic icon loading

pulsoicon-react/all exports every icon by its kebab-case name, but it pulls in all of them, so use it only where you need the full set. To store icon names (in a database, a CMS) and load each one on demand, list the names with pulsoicon-react/names and import the module when it is needed:

import { ICON_NAMES, type IconName } from "pulsoicon-react/names";

const load = (name: IconName) => import(`pulsoicon-react/icons/${name}`);

Check in your bundler’s build output that each icon really becomes its own chunk.

Accessibility

Icons render with aria-hidden="true", because the text of the button or link they sit in already says what it does. Give an icon-only control its own label:

<button aria-label="Delete">
  <Trash2 />
</button>

Nothing moves when the system asks for reduced motion: hover, play and startAnimation() all leave the icon at rest.