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.