Icon Forge
Draw custom icons that look like they came from one professional set, and ship them with no runtime dependencies. You are done when every icon passes lint, you have looked at the 16/20/24px preview and it reads clearly and consistently on light and dark, and the generated components are wired in with accessible usage.
<skill> below means the folder containing this file (e.g. .claude/skills/icon-forge, .cursor/skills/icon-forge, .agents/skills/icon-forge). Scripts need Node ≥18 and no project dependencies.
When to use / when not
- Use: a sidebar, navbar, dashboard, settings page, empty state or toolbar needs icons; the user asks for a custom or missing icon; the user wants to replace emoji/placeholder icons or a heavy icon package with a small own set.
- Don't use: logos and brand marks (trademarks; need a designer), favicons/app icons, illustrations or spot art, photos/raster, animated Lottie. If the user explicitly asks to install a library (e.g. "just use Lucide"), do what they asked; this skill exists for the custom path.
Hard rules (and why)
- Draw every icon yourself; add no icon-library dependency. The user chose own icons for bundle size and a unique look. Don't paste path data from Lucide, Heroicons, Material, etc. (license and originality). Use
references/glyph-cookbook.md(original drawings) as starting geometry. - If the project already uses an icon library, match its grid, stroke, caps and radius (see
scripts/detect.mjsand design-rules §10). New icons must sit next to existing ones without looking foreign. - One set, one style. Same grid, stroke width, caps/joins, radius scale, padding and detail level for every icon. Consistency matters more than any single clever icon.
- Presentation attributes live only on the root
<svg>, colour iscurrentColor, and sources contain no ids, styles, classes, transforms or<g>. Otherwise theming, dark mode, thestrokeWidthprop and inlining break. - Never claim an icon is good without rendering it and viewing the PNG. Geometry that looks right in code routinely fails at 16px: blobs, closed gaps, off-centre shapes, mismatched weight.
- Run the scripts; don't re-implement them. Lint, optimize, preview and codegen are deterministic. Never hand-edit generated files; change
icons/src/and rebuild. - Don't modify the user's dependencies. Preview tooling installs into
~/.cache/icon-forge(--install); SVGO runs vianpxif absent. Ask before adding any package to the project. - Accessible by default. Icons are
aria-hiddenunless labelled; icon-only controls getaria-label; interactive targets ≥44px; meaningful icons ≥3:1 contrast.
Workflow
1. Detect stack and existing style
node <skill>/scripts/detect.mjs .
Note the framework, TypeScript, Tailwind, any icon library (and its style preset), local SVG icons, and the suggested output dir. If an existing set exists, open 2–3 of its SVGs and copy their root numbers. Otherwise use the default: 24 grid, stroke 2, round caps/joins.
2. Plan the list: meaning → metaphor → file name
Write a short table before drawing: label | metaphor (what is drawn) | file name | cookbook start. Example: Projects | kanban board | kanban.svg | kanban. Pick mainstream metaphors (design-rules §7) and one metaphor per meaning. Name files for the object, kebab-case. Ask the user only if a label is ambiguous and the choice changes the UI meaning; otherwise decide and state the choice.
3. Draw on the 24 grid
Create icons/src/<name>.svg with the canonical root (svg-code-rules.md). For each icon:
- Start from the matching keyline: circle r10, square 18 (x/y 3), portrait 16×20, landscape 20×16. Keylines are stroke centrelines.
- Keep the stroke's outer edge ≥1 unit from the canvas edge. Use radius 2 on large corners and 1–1.5 on small ones; keep gaps ≥2 units of clear space.
- Make parts either join exactly or keep a clear gap. Use integer coordinates for 2px strokes. Use as few elements as possible (≤6 is typical).
- Start from the closest
references/glyph-cookbook.mdentry, then adapt it. Copy the geometry from<skill>/assets/cookbook/<name>.svgwhen a glyph fits as-is. - Compute repeated geometry (teeth, rays, star points) with a few lines of JS instead of guessing. Draw the whole set first, then refine. Consistency is judged side by side.
4. Lint
node <skill>/scripts/lint.mjs icons/src
Fix every ✖. Treat ⚠ gap, density, scale and bounds as design problems to fix, not noise. Note each ℹ gap and check it in step 6; tight parallel strokes (bars, lines) smudge at 16px unless they sit 4 units apart. Re-run until it shows PASS with no warnings you can't justify. Flags: --grid, --padding, --mode fill, --strict, --json.
5. Optimize
node <skill>/scripts/optimize.mjs icons/src icons/svg
This rounds numbers, runs SVGO (local install or npx svgo@4; falls back to the built-in canonicalizer offline), re-orders root attributes, and prints bytes per icon plus total gzip. Lint the output too: node <skill>/scripts/lint.mjs icons/svg.
6. Preview and LOOK, then iterate
node <skill>/scripts/preview.mjs icons/svg --out icons/preview.png \
--sidebar "dashboard:Dashboard,kanban:Projects" --active dashboard # add --install the first time if no PNG engine
View the PNGs with your image-viewing tool, in this order:
icons/[email protected]: the set strip and the sidebar mocks. It is small enough to see at full detail.icons/[email protected]: every icon at each size, plus the inspect view. It's tall, so crop it if your viewer downscales.icons/preview.png: true 1× pixels.
Together they show:
- a 24/16px strip on light and dark;
- a sidebar mock with an active item and a collapsed rail;
- per-icon rows at 16/20/24/48;
- a 4× inspect view with grid, blue keylines and the red 1px padding limit.
Answer the self-review questions in design-rules §12. Typical fixes:
| You see | Fix |
|---|---|
| One icon looks lighter/smaller in the strip | enlarge to its keyline, or add a frame/axis (open glyphs read light) |
| One looks heavier/busier | remove details; ≤3 bars, ≤6 teeth, ≤2 text lines |
| A smudge where parts meet at 16px | join exactly or open the gap to ≥2 units |
| Shape hugs one side in the inspect view | re-centre, or nudge 0.5–1 unit for optical balance |
| Metaphor unclear in the collapsed rail | choose a more literal metaphor (step 2) |
Repeat steps 3–6 until a second look finds nothing to fix. Usually 1–3 rounds.
7. Generate framework output
node <skill>/scripts/build.mjs icons/svg --out src/components/icons --framework react --lang ts \
--labels "kanban:Projects,dashboard:Dashboard"
--framework react|vue|svelte|html|all; --lang ts|js; --package @app/icons writes a sideEffects:false package.json. The output is one tree-shakable module per icon, a shared base component (size, color, strokeWidth, absoluteStrokeWidth, title, className), index named exports, and icons.json. HTML gives sprite.svg + usage.html. Details: references/output-formats.md.
Then type-check or build the project if it has a script (npx tsc --noEmit, npm run build) and fix any errors.
8. Wire it in with accessible usage
Use the pattern from references/accessibility-and-usage.md:
<nav aria-label>;aria-current="page"plus colour and background for the active item;- icon
size-5 shrink-0and decorative; - an
aria-labelon icon-only buttons; logout as a<button>; - ≥44px rows; muted colours that still reach 3:1.
Output format
Finish with a short report to the user:
- Icons: table
label → component → metaphorand the folder paths (icons/src,icons/svg, generated dir). - Checks: lint result (errors/warnings), optimize totals (
avg B/icon,gzip total), and the preview path(s) you inspected, plus what you changed after looking. - Usage snippet for their framework, with a11y attributes.
- Notes: style decisions (grid/stroke/preset matched), RTL icons to mirror, and anything you couldn't verify.
Example
User: "Need sidebar icons for Dashboard, Projects, Team, Settings in my Next.js + Tailwind app."
→ detect: React (Next) + TS + Tailwind, no icon library → plan: dashboard/tiles, kanban, users, settings/gear → draw from the cookbook and adapt → lint PASS → optimize: 4 icons, avg ~340 B, ~0.4 KB gzip → preview: Projects looked light, so add bars; re-check → build --framework react into src/components/icons → <DashboardIcon className="size-5 shrink-0" /> in the nav with aria-current.
References
| Open when you need to… | Read |
|---|---|
| choose sizes, keylines, radii, gaps, metaphors, or fix a preview problem | references/design-rules.md |
| write or debug SVG markup, or understand a lint error | references/svg-code-rules.md |
| start from proven geometry for a common UI icon (56 drawn glyphs) | references/glyph-cookbook.md + assets/cookbook/*.svg |
| generate or hand-adapt React/Vue/Svelte/Angular/HTML output, or reduce bundle size | references/output-formats.md |
| write the usage snippet: nav/sidebar patterns, active state, labels, contrast, RTL | references/accessibility-and-usage.md |
| change the shape of generated components (props, class names, a11y defaults) | templates/<framework>/, which build.mjs reads; edit there, never in the output |
| test changes to this skill | evals/evals.json |
Verification checklist (all must be true before you report done)
- No icon library added; every icon drawn for this task or adapted from the cookbook.
- Style matches the project's existing icons (or the default preset) across the whole set.
-
lint.mjsPASS onicons/srcandicons/svg; remaining warnings justified in the report. - You viewed the preview PNG after your last change; 16px light/dark rows are clean; no icon is heavier, lighter or larger than its siblings.
- Components generated by
build.mjs; project type-check/build passes (when available). - Usage: decorative icons hidden, icon-only controls labelled, active state not colour-only, targets ≥44px, contrast ≥3:1.
- Report includes sizes (avg bytes/icon, gzip total) and preview path.