Overview
Lucide Animated icons support both automatic hover animations and programmatic control via React refs. This dual-mode system is implemented using the IconHandle interface and the isControlledRef pattern.
IconHandle Interface
Every icon component exports a corresponding handle interface with two methods:
export interface IconNameHandle {
startAnimation : () => void ;
stopAnimation : () => void ;
}
Available Methods
Triggers the icon’s animation sequence. Starts the “animate” variant state.
Stops the animation and returns to the “normal” variant state.
Basic Usage
To control an icon programmatically, create a ref with the icon’s handle type:
import { useRef } from 'react' ;
import { HeartIcon } from '@/components/ui/icon-name' ;
import type { HeartIconHandle } from '@/components/ui/icon-name' ;
export default function Example () {
const heartRef = useRef < HeartIconHandle >( null );
const handleClick = () => {
heartRef . current ?. startAnimation ();
};
return (
< div >
< HeartIcon ref = { heartRef } />
< button onClick = { handleClick } > Animate Heart </ button >
</ div >
);
}
When a ref is attached to an icon, automatic hover animations are disabled . You must manually trigger animations using startAnimation() and stopAnimation().
Control Flow
The control mode is determined by the isControlledRef pattern:
Implementation Details
From the source code (heart.tsx:21-30):
const controls = useAnimation ();
const isControlledRef = useRef ( false );
useImperativeHandle ( ref , () => {
isControlledRef . current = true ; // Enable controlled mode
return {
startAnimation : () => controls . start ( 'animate' ),
stopAnimation : () => controls . start ( 'normal' ),
};
});
How It Works
Initial State : isControlledRef.current is false
Ref Attachment : When a ref is attached, useImperativeHandle executes
Mode Switch : Sets isControlledRef.current = true
Control Methods : Returns the startAnimation and stopAnimation methods
Mouse Event Behavior
Mouse event handlers check the control mode (bell.tsx:38-56):
const handleMouseEnter = useCallback (
( e : React . MouseEvent < HTMLDivElement >) => {
if ( isControlledRef . current ) {
onMouseEnter ?.( e ); // Just pass the event through
} else {
controls . start ( 'animate' ); // Auto-animate
}
},
[ controls , onMouseEnter ]
);
const handleMouseLeave = useCallback (
( e : React . MouseEvent < HTMLDivElement >) => {
if ( isControlledRef . current ) {
onMouseLeave ?.( e ); // Just pass the event through
} else {
controls . start ( 'normal' ); // Auto-reset
}
},
[ controls , onMouseLeave ]
);
Uncontrolled Mode isControlledRef.current === falseMouse events trigger animations automatically
Controlled Mode isControlledRef.current === trueMouse events are passed to parent handlers
useAnimation Hook
Icons use Framer Motion’s useAnimation() hook to control animations:
import { useAnimation } from 'motion/react' ;
const controls = useAnimation ();
// Start a variant
controls . start ( 'animate' );
// Stop and return to normal
controls . start ( 'normal' );
Multiple Animation Controls
Complex icons may use multiple useAnimation() instances for choreographed effects.
From sparkles.tsx:54-70:
const starControls = useAnimation ();
const sparkleControls = useAnimation ();
const isControlledRef = useRef ( false );
useImperativeHandle ( ref , () => {
isControlledRef . current = true ;
return {
startAnimation : () => {
sparkleControls . start ( 'hover' );
starControls . start ( 'blink' , { delay: 1 }); // Delayed sequence
},
stopAnimation : () => {
sparkleControls . start ( 'initial' );
starControls . start ( 'initial' );
},
};
});
This allows different parts of the icon to animate independently or in sequence.
Advanced Examples
Manual Hover Control
Even in controlled mode, you can implement custom hover behavior:
import { useRef } from 'react' ;
import { BellIcon } from '@/components/ui/icon-name' ;
import type { BellIconHandle } from '@/components/ui/icon-name' ;
export default function CustomHover () {
const bellRef = useRef < BellIconHandle >( null );
return (
< BellIcon
ref = { bellRef }
onMouseEnter = { () => bellRef . current ?. startAnimation () }
onMouseLeave = { () => bellRef . current ?. stopAnimation () }
/>
);
}
Click to Animate
Trigger animations on click instead of hover:
import { useRef } from 'react' ;
import { HeartIcon } from '@/components/ui/icon-name' ;
import type { HeartIconHandle } from '@/components/ui/icon-name' ;
export default function ClickToAnimate () {
const heartRef = useRef < HeartIconHandle >( null );
return (
< HeartIcon
ref = { heartRef }
onClick = { () => heartRef . current ?. startAnimation () }
className = "cursor-pointer"
aria-label = "Like button"
/>
);
}
Focus-Based Animation
Animate on keyboard focus for accessibility:
import { useRef } from 'react' ;
import { SparklesIcon } from '@/components/ui/icon-name' ;
import type { SparklesIconHandle } from '@/components/ui/icon-name' ;
export default function FocusAnimation () {
const sparklesRef = useRef < SparklesIconHandle >( null );
return (
< SparklesIcon
ref = { sparklesRef }
onFocus = { () => sparklesRef . current ?. startAnimation () }
onBlur = { () => sparklesRef . current ?. stopAnimation () }
tabIndex = { 0 }
aria-label = "Special effects"
/>
);
}
Conditional Animation
Animate based on application state:
import { useRef , useEffect } from 'react' ;
import { BellIcon } from '@/components/ui/icon-name' ;
import type { BellIconHandle } from '@/components/ui/icon-name' ;
export default function NotificationBell ({ hasUnread } : { hasUnread : boolean }) {
const bellRef = useRef < BellIconHandle >( null );
useEffect (() => {
if ( hasUnread ) {
bellRef . current ?. startAnimation ();
} else {
bellRef . current ?. stopAnimation ();
}
}, [ hasUnread ]);
return < BellIcon ref = { bellRef } /> ;
}
Timed Animation Sequence
Create animation sequences with delays:
import { useRef , useEffect } from 'react' ;
import { HeartIcon , SparklesIcon } from '@/components/ui/icon-name' ;
import type { HeartIconHandle , SparklesIconHandle } from '@/components/ui/icon-name' ;
export default function AnimationSequence () {
const heartRef = useRef < HeartIconHandle >( null );
const sparklesRef = useRef < SparklesIconHandle >( null );
const playSequence = () => {
heartRef . current ?. startAnimation ();
setTimeout (() => {
sparklesRef . current ?. startAnimation ();
}, 500 );
setTimeout (() => {
heartRef . current ?. stopAnimation ();
sparklesRef . current ?. stopAnimation ();
}, 2000 );
};
return (
< div className = "flex gap-4" >
< HeartIcon ref = { heartRef } />
< SparklesIcon ref = { sparklesRef } />
< button onClick = { playSequence } > Play Sequence </ button >
</ div >
);
}
Controlled Mode with Custom Logic
Combine controlled animations with custom business logic:
import { useRef , useState } from 'react' ;
import { HeartIcon } from '@/components/ui/icon-name' ;
import type { HeartIconHandle } from '@/components/ui/icon-name' ;
export default function LikeButton () {
const heartRef = useRef < HeartIconHandle >( null );
const [ liked , setLiked ] = useState ( false );
const [ count , setCount ] = useState ( 0 );
const handleLike = () => {
heartRef . current ?. startAnimation ();
setLiked ( ! liked );
setCount ( liked ? count - 1 : count + 1 );
// Stop animation after delay
setTimeout (() => {
heartRef . current ?. stopAnimation ();
}, 1000 );
};
return (
< div className = "flex items-center gap-2" >
< HeartIcon
ref = { heartRef }
onClick = { handleLike }
className = { liked ? 'text-red-500' : 'text-gray-400' }
/>
< span > { count } </ span >
</ div >
);
}
Understanding the isControlledRef Pattern
Why useRef Instead of useState?
The control mode uses useRef instead of useState for performance:
const isControlledRef = useRef ( false );
No Re-renders : Changing ref.current doesn’t trigger re-renders
Persistent State : Value persists across renders
Synchronous : Changes are immediately available
Performance : No additional render cycles when toggling mode
The mode only changes once when the ref is first attached via useImperativeHandle, then remains stable for the component’s lifetime.
Ref Lifecycle
Component Mount : isControlledRef.current = false
Ref Attachment : Parent attaches ref to icon
useImperativeHandle Executes : Sets isControlledRef.current = true
Controlled Mode Active : Remains controlled until unmount
Type Safety
TypeScript ensures type-safe control:
import { useRef } from 'react' ;
import type { HeartIconHandle } from '@/components/ui/icon-name' ;
// Correct types
const heartRef = useRef < HeartIconHandle >( null );
heartRef . current ?. startAnimation (); // ✓ Type-safe
heartRef . current ?. stopAnimation (); // ✓ Type-safe
// TypeScript catches errors
heartRef . current ?. invalidMethod (); // ✗ Type error
Best Practices
Use Controlled Mode For
Custom trigger events (click, focus)
Animation sequences
State-dependent animations
Complex interaction patterns
Use Uncontrolled Mode For
Simple hover interactions
Default behavior
Quick implementations
Standard icon displays
Remember: Attaching a ref disables automatic hover animations . If you want both hover and programmatic control, you must manually handle onMouseEnter and onMouseLeave to call startAnimation() and stopAnimation().
Summary
The animation control system provides:
Dual-mode operation : Automatic hover or programmatic control
Simple API : Just two methods - startAnimation() and stopAnimation()
Intelligent switching : isControlledRef pattern automatically detects mode
Full flexibility : Complete control over when and how animations play
Type safety : Full TypeScript support for all operations
This architecture makes Lucide Animated icons versatile enough for simple use cases while powerful enough for complex interactive applications.