Skip to main content
Learn how to create custom animated icons using Motion (formerly Framer Motion) with best practices from the Anicon library.

Icon Component Structure

Every Anicon component follows a consistent structure:
"use client";

import { motion, useReducedMotion } from "framer-motion";

// 1. Props interface
export interface IconMyIconProps extends React.SVGProps<SVGSVGElement> {
  size?: number;
  strokeWidth?: number;
}

// 2. Animation variants
const animationVariants = {
  rest: { /* initial state */ },
  hover: { /* hover animation */ },
  tap: { /* tap/click animation */ }
};

// 3. Component
export function IconMyIcon({ 
  size = 24, 
  strokeWidth = 2, 
  className, 
  ...props 
}: IconMyIconProps) {
  const { onAnimationStart, ...rest } = props;
  const prefersReducedMotion = useReducedMotion();

  return (
    <motion.svg
      {/* SVG attributes */}
      initial={prefersReducedMotion ? false : "rest"}
      whileHover={prefersReducedMotion ? undefined : "hover"}
      whileTap={prefersReducedMotion ? undefined : "tap"}
      variants={animationVariants}
      {...rest}
    >
      {/* Animated paths */}
    </motion.svg>
  );
}

Core Concepts

1. Using Motion Variants

Variants define animation states that can be triggered by user interactions:
const scaleVariants = {
  rest: { 
    scale: 1 
  },
  hover: {
    scale: 1.1,
    transition: {
      duration: 0.3,
      ease: "easeInOut"
    }
  },
  tap: {
    scale: 0.95
  }
};

2. Animating Individual Elements

You can animate specific parts of your icon independently:
const iconVariants = {
  rest: {},
  hover: {}
};

const pathVariants = {
  rest: { rotate: 0 },
  hover: { 
    rotate: 360,
    transition: { duration: 0.5 }
  }
};

<motion.svg variants={iconVariants}>
  <motion.path d="..." variants={pathVariants} />
  <motion.circle variants={circleVariants} />
</motion.svg>

3. Reduced Motion Support

Always respect user preferences for reduced motion:
const prefersReducedMotion = useReducedMotion();

return (
  <motion.svg
    initial={prefersReducedMotion ? false : "rest"}
    whileHover={prefersReducedMotion ? undefined : "hover"}
    whileTap={prefersReducedMotion ? undefined : "tap"}
  >
    {/* ... */}
  </motion.svg>
);

4. Props Filtering

Filter out Motion-specific props to avoid React warnings:
export function IconMyIcon({ size = 24, strokeWidth = 2, className, ...props }: IconMyIconProps) {
  const { 
    onAnimationStart, 
    onAnimationEnd, 
    onDragStart, 
    onDrag, 
    onDragEnd,
    ...rest 
  } = props;
  
  return (
    <motion.svg {...rest}>
      {/* ... */}
    </motion.svg>
  );
}

Animation Patterns

Scale Animation (Pulse, Bounce)

Simple scale effect on hover:
const scaleVariants = {
  rest: { scale: 1 },
  hover: {
    scale: 1.1,
    transition: {
      duration: 0.5,
      repeat: Infinity,
      repeatType: "reverse",
      ease: "easeInOut"
    }
  }
};

Rotation Animation (Spin)

Continuous or triggered rotation:
const rotateVariants = {
  rest: { rotate: 0 },
  hover: {
    rotate: 360,
    transition: {
      duration: 1,
      repeat: Infinity,
      ease: "linear"
    }
  }
};

Path Drawing Animation

Animate SVG path strokes:
const pathVariants = {
  rest: {
    pathLength: 0,
    opacity: 0
  },
  hover: {
    pathLength: 1,
    opacity: 1,
    transition: {
      duration: 0.5,
      ease: "easeInOut"
    }
  }
};

<motion.path
  d="M..."
  variants={pathVariants}
  strokeDasharray="0 1"
/>

Keyframe Animations

Create complex sequences with arrays:
const shakeVariants = {
  rest: { rotate: 0 },
  hover: {
    rotate: [0, -15, 15, -10, 10, -5, 5, 0],
    transition: {
      duration: 0.6,
      ease: "easeInOut"
    }
  }
};

Staggered Animations

Animate children with delays:
const containerVariants = {
  rest: {},
  hover: {
    transition: {
      staggerChildren: 0.1
    }
  }
};

const childVariants = {
  rest: { opacity: 0.5 },
  hover: { opacity: 1 }
};

<motion.svg variants={containerVariants}>
  <motion.circle variants={childVariants} />
  <motion.circle variants={childVariants} />
  <motion.circle variants={childVariants} />
</motion.svg>

Using Animation Config

Anicon provides a centralized animation configuration file at lib/animation-config.ts for consistency:
import { animationConfig } from "@/lib/animation-config";

const myVariants = {
  rest: { scale: 1 },
  hover: {
    scale: animationConfig.scales.grow, // 1.1
    transition: animationConfig.transitions.spring
  }
};

Available Config Values

Transitions:
animationConfig.transitions.spring       // Standard spring
animationConfig.transitions.springBouncy // Bouncy spring
animationConfig.transitions.springSnappy // Quick spring
animationConfig.transitions.tween        // Linear tween
animationConfig.transitions.tweenFast    // Fast tween
Scales:
animationConfig.scales.shrink      // 0.9
animationConfig.scales.shrinkSmall // 0.95
animationConfig.scales.grow        // 1.1
animationConfig.scales.growSmall   // 1.05
animationConfig.scales.growLarge   // 1.2
Rotations:
animationConfig.rotations.small  // 15deg
animationConfig.rotations.medium // 45deg
animationConfig.rotations.large  // 90deg
animationConfig.rotations.full   // 360deg
Distances:
animationConfig.distances.small  // 3px
animationConfig.distances.medium // 5px
animationConfig.distances.large  // 8px
Sequences:
animationConfig.sequences.ring   // [0, -20, 20, -15, 15, -10, 10, 0]
animationConfig.sequences.shake  // [0, -5, 5, -5, 5, 0]
animationConfig.sequences.wiggle // [0, -3, 3, -3, 3, 0]

Complete Examples

Example 1: Heart Icon with Beat Animation

"use client";

import { motion, useReducedMotion } from "framer-motion";

export interface IconHeartProps extends React.SVGProps<SVGSVGElement> {
  size?: number;
  strokeWidth?: number;
}

const beatVariants = {
  rest: { scale: 1 },
  hover: {
    scale: 1.1,
    transition: {
      duration: 0.5,
      repeat: Infinity,
      repeatType: "reverse",
      ease: "easeInOut"
    }
  }
};

export function IconHeart({ size = 24, strokeWidth = 2, className, ...props }: IconHeartProps) {
  const { onAnimationStart, ...rest } = props;
  const prefersReducedMotion = useReducedMotion();

  return (
    <motion.svg
      xmlns="http://www.w3.org/2000/svg"
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={strokeWidth}
      strokeLinecap="round"
      strokeLinejoin="round"
      initial={prefersReducedMotion ? false : "rest"}
      animate={prefersReducedMotion ? false : "rest"}
      whileHover={prefersReducedMotion ? undefined : "hover"}
      whileTap={prefersReducedMotion ? undefined : "tap"}
      className={`outline-none focus:outline-none focus:ring-0 select-none ${className ?? ""}`.trim()}
      variants={beatVariants}
      {...rest}
    >
      <motion.path 
        d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"
      />
    </motion.svg>
  );
}

Example 2: Accessibility Icon with Sway

"use client";

import { motion, useReducedMotion, type Variants } from "framer-motion";

export interface IconAccessibilityProps extends React.SVGProps<SVGSVGElement> {
  size?: number;
  strokeWidth?: number;
}

const limbVariants: Variants = {
  rest: { rotate: 0 },
  hover: {
    rotate: [0, -5, 5, 0],
    transition: {
      duration: 1.5,
      repeat: Infinity,
      ease: "easeInOut",
    },
  },
};

const headVariants: Variants = {
  rest: { y: 0 },
  hover: {
    y: [0, -0.5, 0],
    transition: {
      duration: 1.5,
      repeat: Infinity,
      ease: "easeInOut",
    },
  },
};

export function IconAccessibility({
  size = 24,
  strokeWidth = 2,
  className,
  ...props
}: IconAccessibilityProps) {
  const { onAnimationStart, onAnimationEnd, onDragStart, onDrag, onDragEnd, ...restOptions } = props;
  const prefersReducedMotion = useReducedMotion();

  return (
    <motion.svg
      xmlns="http://www.w3.org/2000/svg"
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={strokeWidth}
      strokeLinecap="round"
      strokeLinejoin="round"
      initial={prefersReducedMotion ? false : "rest"}
      whileHover={prefersReducedMotion ? undefined : "hover"}
      whileTap={prefersReducedMotion ? undefined : "tap"}
      className={`outline-none focus:outline-none focus:ring-0 select-none ${className ?? ""}`.trim()}
      style={{ overflow: "visible" }}
      {...restOptions}
    >
      <motion.circle 
        cx="16" cy="4" r="1" 
        variants={headVariants}
      />
      <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
    </motion.svg>
  );
}

Best Practices

Performance

  • Prefer animating transform and opacity properties
  • Avoid animating width, height, or other layout properties
  • Use will-change sparingly and only when needed
  • Keep animation durations between 0.2s - 0.6s for responsiveness

Accessibility

  • Always implement useReducedMotion() support
  • Provide alternative static states for reduced motion
  • Ensure icons remain usable without animations
  • Test with keyboard navigation

Design

  • Keep animations subtle and purposeful
  • Ensure animations enhance, not distract
  • Match animation style across icon sets
  • Test at different sizes (16px, 24px, 32px, 48px)

Code Organization

  • Define variants outside the component for readability
  • Use TypeScript for type safety
  • Export props interfaces for documentation
  • Follow consistent naming conventions

Troubleshooting

Animation not triggering

Ensure you’re using motion components and passing variants correctly:
// ✅ Correct
<motion.svg variants={myVariants}>
  <motion.path variants={pathVariants} />
</motion.svg>

// ❌ Wrong
<svg>
  <path /> {/* Not a motion component */}
</svg>

Props warning in console

Filter out Motion-specific props:
const { 
  onAnimationStart, 
  onAnimationEnd,
  ...rest 
} = props;

Animation too fast/slow

Adjust transition duration:
hover: {
  scale: 1.1,
  transition: {
    duration: 0.5, // Adjust this
    ease: "easeInOut"
  }
}

Next Steps

Build docs developers (and LLMs) love