While icons animate on hover by default, you can take full control of animations using React refs. This is useful for triggering animations based on custom events, user interactions, or application state.
IconHandle Interface
Every icon component exposes an IconHandle interface through refs with two methods:
export interface IconHandle {
startAnimation : () => void ;
stopAnimation : () => void ;
}
Starts or restarts the icon’s animation sequence
Stops the animation and returns the icon to its normal state
Using Refs for Control
Create a ref and attach it to the icon component. Once attached, you can call methods to control the animation:
import { useRef } from "react" ;
import { HeartIcon , HeartIconHandle } from "lucide-animated" ;
export default function ControlledIcon () {
const heartRef = useRef < HeartIconHandle >( null );
return (
< div >
< HeartIcon ref = { heartRef } />
< button onClick = { () => heartRef . current ?. startAnimation () } >
Start Animation
</ button >
< button onClick = { () => heartRef . current ?. stopAnimation () } >
Stop Animation
</ button >
</ div >
);
}
When a ref is attached, hover animations are automatically disabled. The icon will only animate when you explicitly call startAnimation().
Real-World Examples
Like Button
Notification Badge
Form Validation
Success Message
import { useRef , useState } from "react" ;
import { HeartIcon , HeartIconHandle } from "lucide-animated" ;
export default function LikeButton () {
const heartRef = useRef < HeartIconHandle >( null );
const [ liked , setLiked ] = useState ( false );
const handleLike = () => {
heartRef . current ?. startAnimation ();
setLiked ( ! liked );
};
return (
< button
onClick = { handleLike }
className = { liked ? "text-red-500" : "text-gray-500" }
>
< HeartIcon ref = { heartRef } size = { 24 } />
</ button >
);
}
Implementation Details
Here’s how the imperative control is implemented in the source code:
import { motion , useAnimation } from "motion/react" ;
import { forwardRef , useCallback , useImperativeHandle , useRef } from "react" ;
export interface HeartIconHandle {
startAnimation : () => void ;
stopAnimation : () => void ;
}
const HeartIcon = forwardRef < HeartIconHandle , HeartIconProps >(
({ onMouseEnter , onMouseLeave , className , size = 28 , ... props }, ref ) => {
const controls = useAnimation ();
const isControlledRef = useRef ( false );
// Expose methods through ref
useImperativeHandle ( ref , () => {
isControlledRef . current = true ;
return {
startAnimation : () => controls . start ( "animate" ),
stopAnimation : () => controls . start ( "normal" ),
};
});
// Disable hover when controlled via ref
const handleMouseEnter = useCallback (
( e : React . MouseEvent < HTMLDivElement >) => {
if ( isControlledRef . current ) {
onMouseEnter ?.( e );
} else {
controls . start ( "animate" );
}
},
[ controls , onMouseEnter ]
);
const handleMouseLeave = useCallback (
( e : React . MouseEvent < HTMLDivElement >) => {
if ( isControlledRef . current ) {
onMouseLeave ?.( e );
} else {
controls . start ( "normal" );
}
},
[ controls , onMouseLeave ]
);
return (
< div
onMouseEnter = { handleMouseEnter }
onMouseLeave = { handleMouseLeave }
{ ... props }
>
< motion.svg
animate = { controls }
variants = { {
normal: { scale: 1 },
animate: { scale: [ 1 , 1.08 , 1 ] },
} }
transition = { {
duration: 0.45 ,
repeat: 2 ,
} }
>
{ /* SVG paths */ }
</ motion.svg >
</ div >
);
}
);
Type-Safe Refs
Each icon exports its own handle type for type safety:
import { HeartIconHandle } from "lucide-animated" ;
import { BellIconHandle } from "lucide-animated" ;
import { CheckIconHandle } from "lucide-animated" ;
This ensures you get proper autocomplete and type checking when using refs.