Beautiful icons. Meaningful motion. Copy and ship.
A production-quality, open-source library of hand-animated Lucide + Phosphor icons for React, plus the marketing/gallery/playground website that showcases it.
Live site · Documentation · Playground · Download tarball
This is a monorepo with two things:
/app
├── packages/motive-icons/ # The publishable npm package (33 animated icons)
│ ├── src/
│ │ ├── IconShell.jsx # Universal wrapper w/ controlled active state
│ │ ├── hooks/ # useIconTrigger — hover/click/auto/focus
│ │ ├── animations/ # 33 hand-crafted <AnimatedXxx /> components
│ │ ├── index.js # Barrel exports
│ │ └── index.d.ts # TypeScript declarations
│ ├── dist/ # Built output (ESM + CJS + .d.ts)
│ ├── package.json
│ └── tsup.config.js # Build configuration
│
├── frontend/ # The website (React + CRA + Tailwind)
│ ├── src/
│ │ ├── pages/ # Home · Icons · Playground · Docs · NotFound
│ │ ├── components/
│ │ │ ├── layout/ # Navbar, Footer
│ │ │ ├── shared/ # IconCard, IconDetailModal, CodeBlock, Sponsors
│ │ │ └── ui/ # shadcn/ui primitives (Dialog, Slider, Switch…)
│ │ ├── registry/ # Central iconRegistry — drives search/filters/gallery
│ │ ├── context/ # ThemeContext (dark/light)
│ │ ├── constants/testIds.js
│ │ └── App.js
│ ├── public/
│ │ └── motive-icons-0.1.0.tgz # Latest built tarball, served for install
│ └── package.json
│
├── backend/ # FastAPI (unused for now — placeholder)
├── memory/ # Product docs (PRD.md)
└── README.md # ← you are here
The website imports icons from the motive-icons package — so every animation you see on the site is the exact same code a consumer would get after yarn add motive-icons.
| Tool | Version | Why |
|---|---|---|
| Node | ≥ 18.x | Website + package build |
| Yarn | 1.22.x | Package manager (do NOT use npm) |
| Python | ≥ 3.10 | Optional — backend placeholder |
| MongoDB | ≥ 6.0 | Optional — backend placeholder |
Check:
node --version # v18+ ideally v20+
yarn --version # 1.22.xYarn Classic is required.
npmis not supported and will produce a different lockfile.
The website consumes the built motive-icons package tarball, so the package must be built before the website is installed. The bootstrap script handles the whole chain:
# 1. Clone
git clone https://github.com/YOUR_ORG/motive.git
cd motive
# 2. Bootstrap everything (builds the package + installs the website)
./scripts/bootstrap.sh
# 3. Start the dev server
cd frontend
yarn startThe site is available at http://localhost:3000. Hot-reload is enabled — edits under frontend/src/ refresh automatically.
[1/5] Installs motive-icons build tooling (packages/motive-icons/)
[2/5] Builds the package → dist/index.{js,cjs,d.ts}
[3/5] Packs a fresh tarball → motive-icons-<v>.tgz
[4/5] Copies the tarball into frontend/public/ (so it's downloadable)
[5/5] Installs website dependencies (frontend/ — creates .env from .env.example if missing)
# 1. Build the package
cd packages/motive-icons
yarn install
yarn build
npm pack # → motive-icons-0.1.0.tgz
# 2. Publish the tarball for the site
cp motive-icons-0.1.0.tgz ../../frontend/public/
# 3. Install the website
cd ../../frontend
cp .env.example .env # only if you don't have one
yarn install
yarn start # http://localhost:3000Yarn Classic only —
frontend/package.jsonreferences the local package with the relativefile:../packages/motive-icons/motive-icons-0.1.0.tgzprotocol. npm's semantics forfile:differ; use yarn.
The website consumes the built tarball at frontend/public/motive-icons-<v>.tgz. When you edit any icon animation or the IconShell, rebuild the package and re-install it into the site:
# From repo root — one command handles everything
./scripts/bootstrap.shOr do it manually:
# 1. Rebuild the package
cd packages/motive-icons
yarn build
npm pack # → motive-icons-<v>.tgz
# 2. Copy the fresh tarball into the site (so downloads + install stay in sync)
cp motive-icons-*.tgz ../../frontend/public/
# 3. Force-reinstall in the website (yarn caches by content hash, so bump/pack again if unchanged)
cd ../../frontend
rm -rf node_modules/motive-icons
yarn add file:../packages/motive-icons/motive-icons-0.1.0.tgz --forceReload the dev server and your changes are live.
Tip: for tighter iteration you can
yarn linkthe package (cd packages/motive-icons && yarn link, thencd frontend && yarn link motive-icons). Undo withyarn unlink.
Adding an icon is three edits:
touch packages/motive-icons/src/animations/AnimatedRocket.jsx// packages/motive-icons/src/animations/AnimatedRocket.jsx
import { motion } from "framer-motion";
import IconShell from "../IconShell";
export default function AnimatedRocket(props) {
return (
<IconShell {...props} label="Rocket">
{() => (
<motion.g
style={{ originX: "12px", originY: "22px" }}
variants={{
inactive: { y: 0 },
active: {
y: [0, -4, 0],
transition: { duration: 0.5, ease: [0.34, 1.56, 0.64, 1] },
},
}}
>
{/* Replace with your SVG paths — must fit a 24×24 viewBox */}
<path d="M12 2l4 8h-8l4-8z" />
<path d="M8 10v8l4 4 4-4v-8" />
</motion.g>
)}
</IconShell>
);
}// packages/motive-icons/src/index.js
export { default as AnimatedRocket } from "./animations/AnimatedRocket";Also add a type entry:
// packages/motive-icons/src/index.d.ts
export declare function AnimatedRocket(props: AnimatedIconProps): JSX.Element;// frontend/src/registry/iconRegistry.js
import { AnimatedRocket } from "motive-icons";
// …add to iconRegistry array:
{
name: "Rocket",
library: "Lucide",
category: "Interface",
tags: ["launch", "start"],
animation: "Directional",
description: "Rocket lifts off with a subtle bounce.",
component: AnimatedRocket,
},Rebuild the package (see the previous section) and the icon automatically appears in the gallery, search, filters, and playground — no other changes needed.
- Every animation should reflect the meaning of the icon (bell swings, arrows travel, trash lids open). Avoid generic scale/rotate on everything.
- Duration: 150 – 600 ms for most micro-interactions.
- Reset cleanly — repeat hover must always work.
- Respect
prefers-reduced-motion(the shell already does this globally). - Keep the SVG geometry recognizable — copy the Lucide/Phosphor path where possible so users get consistent visuals.
We ❤️ contributions. The full workflow:
- Fork the repo on GitHub.
- Create a branch off
main:
git checkout -b feat/animated-rocket - Add your icon following the three-step flow above.
- Test locally — hover it on
/icons, tweak in/playground. - Commit with a conventional-commit message:
git commit -m "feat(icons): add AnimatedRocket" - Push and open a pull request describing the animation intent.
- A maintainer will review the animation quality, code style, and register it for the next release.
- Icons: default stroke width
2, 24×24 viewBox,stroke="currentColor",strokeLinecap="round",strokeLinejoin="round". - Components: named default export per file, prop destructure at the top.
- CSS: Tailwind utility classes; avoid inline styles unless dynamic. No emojis in the UI.
- Testing IDs: any interactive element on the website must expose a
data-testidregistered infrontend/src/constants/testIds.js. - Commits: Conventional Commits —
feat:,fix:,docs:,refactor:,chore:.
Open an issue with:
- A short title (e.g. “Bell animation stutters on second hover”).
- Steps to reproduce OR the Lucide/Phosphor icon name you want animated.
- Screenshots or a short screen recording if it’s visual.
yarn start # dev server (http://localhost:3000)
yarn build # production build → build/
yarn test # run tests (Jest via CRA)yarn build # build ESM + CJS + copy .d.ts → dist/
npm pack # create motive-icons-<version>.tgz
npm publish --access public # publish to npmjs.org (requires auth)Any static hosting works — the site is a plain CRA build.
cd frontend
yarn build # outputs to build/
# Deploy the build/ folder to Vercel, Netlify, Cloudflare Pages, S3+CloudFront, etc.The tarball at public/motive-icons-0.1.0.tgz is copied as-is into the build, so yarn add https://your-domain/motive-icons-0.1.0.tgz will keep working post-deploy.
- Bump the version in
packages/motive-icons/package.json. cd packages/motive-icons && yarn build && npm pack.- Copy the tarball into
frontend/public/. - Update the version string in
frontend/src/pages/Docs.jsx(PACKAGE_VERSION+TARBALL_URL). npm publish --access public(once npm credentials are configured).- Tag the release:
git tag v0.1.1 && git push --tags.
- Package: React 18, framer-motion, built with tsup (esbuild)
- Website: React 18, React Router, Tailwind CSS, Framer Motion, shadcn/ui, Sonner
- Icons library: Lucide React + Phosphor Icons (for reference geometry)
- Font: Satoshi (via Fontshare) + JetBrains Mono for code
- Design accent:
#8B5CF6(violet), inspired by Render.com
MIT © Motive contributors — free to use, modify, and distribute.
If Motive helps you ship, consider sponsoring on GitHub or featuring your company logo on the site → see the Sponsors section.
Made with ♥ for the community. Built in public.