YoujIcon
Animate lucide-react icons with the same triggers as Youj icons.
Use YoujIcon to animate a lucide-react icon with hover, mount or loop triggers. When a Youj icon fits, use it for animation designed around that icon’s meaning.
Installation
npm i @youj/iconsFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
This integration is optional. Youj icons need no Lucide dependency. To use the Lucide examples below, add it to your app:
npm i lucide-reactUsage
Pass the lucide component itself, not an element, and pick an effect.
import { Rocket } from 'lucide-react'
import { YoujIcon } from '@youj/icons'
export function DeployButton() {
return (
<button type="button">
<YoujIcon icon={Rocket} effect="bounce" size={16} />
Deploy
</button>
)
}It plays on hover by default and follows the same trigger rules as Youj's icons: hovering or keyboard-focusing the button bounces the rocket. size and strokeWidth pass through to the lucide icon.
Examples
Effects
Seventeen move the whole glyph, and five animate each stroke's pathLength.
| Effect | What it does |
|---|---|
pop | Squeezes, pops past full size and settles. The default. |
wiggle | A damped rotation, back and forth. |
spin | One full turn, always forwards. Two when strong. |
nudge | Out and back along direction. |
pulse | Swells and dims, then returns. |
ring | Swings from near the top, like a bell. |
bounce | Two hops, the second smaller. |
flip | A coin turned once on its vertical axis, with perspective. |
swing | A pendulum hung from the top edge, dying away. |
float | Lifts, then bobs for as long as it's active. |
heartbeat | A strong beat, a softer one, then a long release. |
shake | A hard horizontal shake that decays, for errors. |
jelly | Squash and stretch on the ground line. Use it sparingly. |
orbit | One lap around its own center. |
tilt | Cocks to one side and holds while active. |
zoom-in | Dips away and returns from small. On mount, arrives from small. |
zoom-out | Dips away and lands from large, like a stamp. On mount, arrives from large. |
draw | Re-traces every stroke. |
draw-in | Rests undrawn, draws on while active, and retracts when it ends. |
draw-out | Strokes run out through their ends while active, and grow back after. |
draw-sequence | Writes the icon one stroke at a time, at a constant pen speed. |
stagger-draw | A wave through the strokes: each rewinds and redraws in turn, so the icon never goes blank. |
Four effects hold while active instead of playing once: float, tilt, draw-in and draw-out. The rest are one-shots. Every effect starts from the icon's current pose, so triggering it again mid-flight carries on from where the icon is instead of jumping.
Intensity
intensity scales each effect in the way that suits it.
- Transforms: the amplitude is 0.55× at
subtle, 1× atdefaultand 1.6× atstrong. - Draws: the pace is
base,slowordeliberate, anddrawandstagger-drawoffset their strokes by thetight,baseorloosestagger step.draw-sequencetakes 1.4, 2 or 2.8 timesdeliberatefor the whole icon. - Turns:
spinandflipturn once, and twice atstrong.
<YoujIcon icon={CircleAlert} effect="shake" intensity="strong" />Direction
direction steers nudge: up, down, left or right, the default. Left and right follow the reading direction, so under dir="rtl" they swap. orbit and tilt mirror the same way. A glyph that's already mirrored with CSS scale isn't flipped twice.
<YoujIcon icon={ArrowRight} effect="nudge" direction="right" />Triggers
YoujIcon takes the same trigger values as every Youj icon: hover (the default), mount, loop and none. draw-in rests undrawn, so it defaults to mount and draws on as it appears.
A controlled active plays the effect each time it turns true, and a held effect holds for as long as it stays true. That suits effects that answer your app rather than the pointer:
import { CloudUpload } from 'lucide-react'
import { YoujIcon } from '@youj/icons'
// Bobs while a sync runs, and settles when it ends.
<YoujIcon icon={CloudUpload} effect="float" active={syncing} />To replay a one-shot for every event, even when the state stays the same, remount it with a new key and let mount play it:
// Shakes once for every failed attempt.
<YoujIcon key={attempts} icon={CircleAlert} effect="shake" trigger="mount" aria-label="Invalid" />float pauses offscreen and in hidden tabs even while active or mount holds it on.
Reference
Propsicon, effect, trigger… (11)
Props
| Prop | Type | |
|---|---|---|
iconRequired | LucideLikeIconAny lucide-react icon component (or one shaped like it). | – |
effect | YoujIconEffect'pop''wiggle''spin''draw''nudge''pulse''ring''bounce''flip''swing''float''heartbeat''shake''jelly''orbit''tilt''zoom-in''zoom-out''draw-in''draw-out''draw-sequence''stagger-draw'default 'pop' | 'pop' |
trigger | IconTrigger'hover''mount''loop''none'Default hover; | – |
active | booleanControlled: plays the effect whenever this turns true (held effects hold while it stays true). | – |
intensity | YoujIconIntensity'subtle''default''strong'default 'default' | 'default' |
direction | YoujIconDirection'up''down''left''right'default 'right' | 'right' |
size | number | stringdefault 24 | 24 |
strokeWidth | numberdefault 1.5 | 1.5 |
className | string | – |
style | CSSProperties | – |
aria-label | stringGives the icon role="img". Without it the icon is decorative (aria-hidden). | – |
MotionOne-shots, longer one-shots, nudge… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| One-shotspop, wiggle, pulse, ring, bounce, shake, jelly, zoom-in, zoom-out | duration.deliberate560ms | 560ms | |
| Longer one-shotsheartbeat ×1.25, orbit ×1.4, swing ×1.5 | duration.deliberate560ms | 560ms | |
| Nudgeout and back | duration.slowease.emphasized340ms · cubic-bezier(0.6, 0, 0.1, 1) | 340mscubic-bezier(0.6, 0, 0.1, 1) | |
| Spinone turn | duration.deliberateease.enter560ms · cubic-bezier(0.16, 1, 0.3, 1) | 560mscubic-bezier(0.16, 1, 0.3, 1) | |
| Flipone turn in 3D | duration.deliberateease.emphasized560ms · cubic-bezier(0.6, 0, 0.1, 1) | 560mscubic-bezier(0.6, 0, 0.1, 1) | |
| Tiltcocks and holds | spring.snappy0.2s, bounce 0 | 0.2s, bounce 0 | |
| Floatlifts, then bobs on a 3 × deliberate cycle | duration.slowease.enter340ms · cubic-bezier(0.16, 1, 0.3, 1) | 340mscubic-bezier(0.16, 1, 0.3, 1) | |
| Drawsthe default pace | duration.slowease.emphasized340ms · cubic-bezier(0.6, 0, 0.1, 1) | 340mscubic-bezier(0.6, 0, 0.1, 1) | |
| Stroke offsetbetween strokes at the default intensity | stagger.base40ms step | 40ms step | |
| Releaseback to rest when the trigger ends | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
The icon stays still and fully drawn. A controlled draw-in that's off, or draw-out that's on, stays hidden while that state holds.
Reduced motion
The icon stays still and fully drawn, so it reads exactly like the plain lucide icon. A controlled draw-in that's off, or draw-out that's on, stays hidden, because hidden is the state it describes.
AccessibilityKeyboard, ARIA, focus
Accessibility
- The icon is decorative by default (
aria-hidden) and never focusable. Inside a labeled button, leave it that way. aria-labelmakes itrole="img"with that name, for an icon that stands alone and means something, like an error mark.- Hover effects also play on keyboard focus of the trigger, so keyboard users see the same feedback.
- An effect doesn't announce anything. If
shakereports an error, the error text still has to be in the page, tied to its field.