Skip to main content

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

startAnimation
() => void
Triggers the icon’s animation sequence. Starts the “animate” variant state.
stopAnimation
() => void
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

  1. Initial State: isControlledRef.current is false
  2. Ref Attachment: When a ref is attached, useImperativeHandle executes
  3. Mode Switch: Sets isControlledRef.current = true
  4. 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

  1. Component Mount: isControlledRef.current = false
  2. Ref Attachment: Parent attaches ref to icon
  3. useImperativeHandle Executes: Sets isControlledRef.current = true
  4. 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.