Search 150+ icons built with Motion and Lucide. Hover or tap an icon to preview its animation, then copy the React component or download the .tsx file.
The library has 155 icons drawn from Lucide's SVG paths and animated with Motion. Each one is a single, self-contained React component in a .tsx file: it renders an inline SVG and plays its animation while the pointer is over it. Hover or tap an icon in the grid to preview it, then copy the component source, download that icon's .tsx file, or use Download all to get every icon as animated-icons.zip, with one .tsx file per icon in an icons folder.
The only dependency is the motion package; you do not need Lucide installed, because the SVG paths are written into each file. Save the file in your components folder under the icon's name and import it by name, since every file uses a named export. The files start with "use client", so in the Next.js App Router you can import them from a server component and they will render as client components.
The files are TypeScript. In a JavaScript project, rename the file to .jsx and delete the props interface, the import type line, and the type annotations; the rest is plain JSX.
// npm install motion
import { Rocket } from "@/components/icons/Rocket";
export function LaunchButton() {
return (
<button type="button" className="launch-button">
<Rocket width={20} height={20} stroke="currentColor" aria-hidden="true" />
Launch
</button>
);
}Every icon accepts width and height (both 28 by default), stroke (#ffffff by default), and strokeWidth (2 by default). The white default suits dark interfaces like this site, but on a light background the icon is invisible until you pass a color. Passing stroke="currentColor" makes the icon follow the text color of its parent, which also makes hover and dark-mode color changes work without extra props.
Any other SVG attribute you pass, such as className, style, or aria-hidden, goes onto the <svg> element. The SVG sits inside a <div> wrapper with 8px of padding, a pointer cursor, user-select: none, and flex centering, and the hover handlers are attached to that wrapper. Two consequences: the hover area is the whole padded box, not just the strokes, and the icon takes up 16px more than its width and height in each direction. In a tight button or toolbar, edit the style object in the file or remove the padding.
Each file defines Motion variants named normal and animate and drives them with useAnimation. Moving the pointer onto the wrapper starts animate; moving it off returns to normal. Some icons play once per hover and others loop while the pointer stays: 36 of the 155 files use repeat: Infinity. Activity, for example, redraws its pulse line with a 1.5 second draw followed by a 0.5 second pause. To change speed or easing, edit the transition inside the variants object at the top of the file.
Touch screens have no hover. Mobile browsers send compatibility mouse events after a tap, so an icon usually starts animating when tapped and stops when something else is tapped. The wrapper is not focusable, so keyboard users never trigger the animation. If the motion should respond to a whole button rather than the icon, move the trigger out of the icon: add a prop and start the controls from an effect, then set that prop from the button's hover and focus handlers.
// Inside the icon file: let the parent decide when to animate
import { useEffect } from "react";
const Rocket = ({ active = false, ...props }: RocketProps & { active?: boolean }) => {
const controls = useAnimation();
useEffect(() => {
controls.start(active ? "animate" : "normal");
}, [active, controls]);
// ...remove onMouseEnter and onMouseLeave from the wrapper div
};
// In the parent
const [active, setActive] = useState(false);
<button
onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)}
onFocus={() => setActive(true)}
onBlur={() => setActive(false)}
>
<Rocket active={active} stroke="currentColor" aria-hidden="true" />
Launch
</button>The SVG has no title or role. Next to a visible text label, pass aria-hidden="true" so screen readers skip it. In an icon-only button, put the accessible name on the button with aria-label="Download", not on the SVG.
The icons do not check prefers-reduced-motion themselves. Wrapping your app in <MotionConfig reducedMotion="user"> handles part of it: Motion then turns off transform animations (x, y, scale, rotate) for people who ask for reduced motion, but it keeps other values animating. That leaves path-drawing icons such as Activity, which animate pathLength and opacity, still moving. To stop every icon, read useReducedMotion in the file and skip the start call.
import { motion, useAnimation, useReducedMotion } from "motion/react";
// inside the component
const controls = useAnimation();
const reduceMotion = useReducedMotion();
<div
onMouseEnter={() => {
if (!reduceMotion) controls.start("animate");
}}
onMouseLeave={() => controls.start("normal")}
>Every icon imports from the same package, motion/react (almost always just motion and useAnimation), so Motion is added to your bundle once no matter how many icons you use; each additional icon only adds its own paths and variants. If your project already depends on framer-motion, change the import to "framer-motion", which exports the same names, instead of installing a second copy of the library. Nothing animates until a hover starts, so a page full of idle icons does no animation work. If you only need a few static icons, plain Lucide is lighter; use these where the motion adds feedback, such as a copy button that confirms or a download icon that moves on hover.
Updated