What it does
itshover is an animated icon library for React. 265 icons, each a standalone component that animates on hover using motion/react. You install them one at a time through the shadcn CLI.
Why I starred it
Most icon libraries give you static SVGs. Some offer animation as an afterthought — a CSS class you toggle, maybe a Lottie file. itshover flips this: the animation is the component. Every icon ships with its own start and stop functions, wired to hover by default but also exposed through a ref for programmatic control.
What caught my eye is the distribution model. Instead of npm install itshover and importing from a barrel file, each icon is a self-contained .tsx file you add through npx shadcn@latest add. No tree-shaking guesswork. You get exactly the icons you use, and you own the source.
How it works
The entire library runs on one pattern, defined in icons/types.ts:
export interface AnimatedIconProps {
size?: number | string;
color?: string;
strokeWidth?: number;
className?: string;
disableHover?: boolean;
}
export interface AnimatedIconHandle {
startAnimation: () => void;
stopAnimation: () => void;
}
Every icon is a forwardRef component. Inside, useAnimate from motion/react gives you a scoped animation controller. The icon defines a start function (what happens on hover) and a stop function (how it resets), then exposes both through useImperativeHandle. That ref-based API means you can trigger animations from outside — form submissions, state changes, whatever.
The animations themselves range from simple to surprisingly detailed. The GitHub icon does a gentle scale-and-rotate wobble:
const start = async () => {
await animate(
".github-icon",
{ scale: [1, 1.1, 1], rotate: [0, -5, 5, 0] },
{ duration: 0.5, ease: "easeInOut" },
);
};
The rocket icon (icons/rocket-icon.tsx) is more involved — it launches the body diagonally off-screen while fading out the flame trail, then instantly repositions from the opposite corner and flies back in. That takes a multi-step Promise.all sequence with zero-duration resets between phases:
const start = async () => {
await Promise.all([
animate(".rocket-upper",
{ x: [0, 40], y: [0, -40], opacity: [1, 0] },
{ duration: 0.35, ease: "easeIn" }),
animate(".rocket-flame",
{ x: [0, -20], y: [0, 20], scale: [0.8, 1.2], opacity: [0.6, 0] },
{ duration: 0.25, ease: "easeOut", delay: 0.05 }),
]);
await animate(".rocket-upper", { x: -40, y: 40, opacity: 0 }, { duration: 0 });
animate(".rocket-upper", { x: 0, y: 0, opacity: 1 }, { duration: 0.25, ease: "easeOut" });
};
The copy icon takes a different approach — instead of animating the whole SVG, it targets only the front rectangle (.front-copy) with a subtle x: [0, 2, 0], y: [0, 2, 0] nudge that looks like the page is being peeled away. Small, but it communicates the action.
The registry pipeline
The shadcn distribution is handled by scripts/generate-registry.ts. It scans the icons/ directory, filters out index.ts and types.ts, and generates a registry.json where every entry declares motion as its only dependency:
function generateRegistryItem(filename: string): RegistryItem {
return {
name: fileToRegistryName(filename),
type: "registry:ui",
dependencies: ["motion"],
files: [
{ path: `icons/${filename}`, type: "registry:ui" },
{ path: "icons/types.ts", type: "registry:ui" },
],
};
}
Each icon ships with the shared types.ts, so the consuming project gets the type definitions automatically. The script also cross-validates against icons/index.ts to catch icons that exist as files but aren't registered in the master ICON_LIST, and vice versa. Running npm run registry:build regenerates registry.json and then calls shadcn build to produce individual JSON files under public/r/.
The search on the website uses Fuse.js with weighted keys — icon names get 3x weight, keywords get 2x. Each icon in ICON_LIST carries a keywords array for discoverability. The fuzzy threshold is 0.3 with ignoreLocation: true, which is permissive enough to find github when you type git.
Using it
npx shadcn@latest add https://itshover.com/r/rocket-icon.json
This drops rocket-icon.tsx and types.ts into your components directory. Then:
import RocketIcon from "@/components/ui/rocket-icon";
// Hover-animated by default
<RocketIcon size={32} color="#0ea5e9" />
// Programmatic control via ref
const ref = useRef<AnimatedIconHandle>(null);
<RocketIcon ref={ref} />
<button onClick={()=> ref.current?.startAnimation()}>Launch</button>
The only runtime dependency is motion (the motion/react package). No icon sprite sheets, no font loading, no CSS imports.
Rough edges
No tests. Zero. The repo has ESLint, Prettier, and TypeScript checking via npm run check, but no test runner configured. For a library of 265 animation components, that is a lot of trust in manual QA.
The dependency list includes mongoose and axios in package.json, which are clearly for the website (sponsor data, icon request submissions), not the icons. But since you install individual icons via the registry — not the npm package — this does not affect consumers. It is messy, though.
Animation consistency varies. Some icons use motion.svg directly as the root, others wrap in a motion.div. The GitHub icon animates the whole <g> group; the copy icon animates individual <path> elements. There is no shared animation utility — each icon reinvents its timing curves and durations. A conventions file or shared presets would help contributors maintain consistency as the library grows.
Documentation is the README and the ARCHITECTURE.md. No per-icon docs, no Storybook, no visual regression testing. You browse the website to see what animations look like, but if you want to customize timing, you read the source.
Bottom line
If you are building a React app and want icons that respond to interaction without rigging your own motion animations, itshover gives you 265 of them with a copy-paste distribution model. The pattern is solid, the animations are thoughtful, and the shadcn registry approach means zero bloat. Just accept that you are adopting code, not a versioned dependency.