{"slug":"icon-ops","title":"icon-ops","summary":"Source, vet, normalize and ship SVG icons for web UI - set selection, licence and trademark traps, currentColor theming, sprite/inline delivery, and accessibility. Triggers on: icon, icons, svg icon, find an icon, add an icon, icon set, icon library, iconify, lucide, heroicons, p","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-30T19:36:46.63444Z","repo":{"url":"https://github.com/0xDarkMatter/claude-mods","stars":43,"forks":7,"license":"MIT","updatedAt":"2026-09-30T15:18:48Z"},"bodyHtml":"<hr>\n<h2>name: icon-ops\ndescription: \"Source, vet, normalize and ship SVG icons for web UI - set selection, licence and trademark traps, currentColor theming, sprite/inline delivery, and accessibility. Triggers on: icon, icons, svg icon, find an icon, add an icon, icon set, icon library, iconify, lucide, heroicons, phosphor, tabler, feather, material symbols, font awesome, simple icons, brand logo, icon sprite, svg sprite, symbol use, currentColor, icon won't change colour, icon font, icon accessibility, aria-hidden icon, icon-only button, icon size, icons look inconsistent, mixed icon sets, normalize svg, strip svg cruft, optimise svg, brandfetch, company logo, client logo, logo by domain, brand assets api, logo api, thesvg, brand icon, brandmark, brand mark, find a logo, logo wall, partner logo, greyscale logo, grayscale, tint a logo, reverse out, knockout, mono logo, dark mode logo, favicon, app icon, apple-touch-icon, maskable icon, svg id collision.\"\nlicense: MIT\nallowed-tools: \"Read Write Bash\"\nmetadata:\nauthor: claude-mods\nrelated-skills: svg-brand-tint-ops, color-ops, tailwind-ops</h2>\n<h1>icon-ops</h1>\n<p>Getting an icon onto a page is easy. Getting one that themes correctly, carries\nthe right licence, matches the twelve icons beside it, and behaves for a screen\nreader is where the work actually is.</p>\n<h2>Helps with</h2>\n<p>An icon that won't change colour on hover, in dark mode, or when the theme\nswitches — almost always a hardcoded <code>#000</code> in the file where <code>currentColor</code>\nshould be.</p>\n<p>A UI where the icons \"look off\" without an obvious cause. Usually two icon sets\nmixed: different grid size, different stroke width, different corner language.\nIndividually fine, together visibly wrong.</p>\n<p>Choosing an icon set at the start of a project, when the choice is cheap, rather\nthan after 60 icons are embedded.</p>\n<p>Using a brand logo — GitHub, Google, a client's mark — and needing to know\nwhether you actually may. The file licence does not answer this; trademark does.</p>\n<p>Needing a logo for an arbitrary company that no icon set carries. That is a\ndifferent category from icon sets — a runtime lookup by domain, not a committed\nglyph — with its own quota, caching and trademark consequences.</p>\n<p>Icon-only buttons that a screen reader announces as \"button\", or announces\ntwice. Both come from putting the accessible name in the wrong place.</p>\n<p>Vendor SVGs carrying Inkscape/Figma metadata, fixed <code>width</code>/<code>height</code> that fights\nCSS, and inline styles that resist theming.</p>\n<p>Deciding between inline SVG, a <code>&lt;symbol&gt;</code> sprite, framework components, and an\nicon font — and discovering too late that <code>&lt;img src=\"icon.svg\"&gt;</code> cannot be\nrecoloured at all.</p>\n<p>A sprite that renders nothing in production but worked locally (the external\n<code>&lt;use&gt;</code> CORS trap).</p>\n<p>Two inlined logos where the second one's gradient bleeds into the first. Both\nfiles declared <code>id=\"a\"</code>; the last definition in the document wins for the whole\npage. Brand marks hit this constantly because they carry gradients.</p>\n<p>Needing a mark in grey, knocked out of a dark header, or in one brand ink — and\nwanting to know whether to generate it or use the owner's published variant.</p>\n<p>A logo wall where one wide wordmark dominates because everything was set to the\nsame <code>width</code>.</p>\n<p>Favicons and app icons — the modern four-file set, and why an Android maskable\nicon gets its edges cropped.</p>\n<h2>The core technique</h2>\n<p><strong><code>fill=\"currentColor\"</code> is the whole game.</strong> An icon that inherits the CSS\n<code>color</code> of its context gets hover, focus, disabled, dark mode, and every future\ntheme for free, with no icon-specific CSS. An icon with a baked-in hex breaks all\nof them simultaneously, and each one gets \"fixed\" separately later.</p>\n<p>Everything else in this skill exists to get icons into that state and keep them\nthere.</p>\n<h2>Workflow</h2>\n<h3>1. Choose the set before sourcing anything</h3>\n<p>Lock four decisions; they constrain every icon that follows:</p>\n<table>\n<thead>\n<tr>\n<th>Decision</th>\n<th>Options</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Grid</td>\n<td>24 (most common) · 20 · 16</td>\n</tr>\n<tr>\n<td>Family</td>\n<td>stroke · filled · both-as-matched-pair</td>\n</tr>\n<tr>\n<td>Stroke width</td>\n<td>1.5 · 2 — <strong>must be identical across the set</strong></td>\n</tr>\n<tr>\n<td>Corner language</td>\n<td>round caps/joins · square</td>\n</tr>\n</tbody>\n</table>\n<p>Sensible defaults: <strong>Lucide</strong> (ISC, 24-grid, stroke 2) for a general UI,\n<strong>Heroicons</strong> (MIT) when you want matched outline/solid/mini tiers, <strong>Phosphor</strong>\n(MIT) when you need multiple weights in one family.</p>\n<p>Reach for a second set only when the first genuinely lacks the concept — then\nmatch grid and stroke width and expect to redraw. Prefer a near-neighbour\nconcept from your set over an exact match from a foreign one.</p>\n<p>Full comparison table, licences, and the aggregator problem →\n<a href=\"references/icon-sources.md\"><code>references/icon-sources.md</code></a>.</p>\n<h3>2. Vet the licence — two traps</h3>\n<p><strong>Brand marks are trademarks regardless of file licence.</strong> Simple Icons ships\nbrand logos under CC0, but the marks remain their owners' property. Nominative\nuse (\"Sign in with GitHub\") is fine; implying endorsement, recolouring a mark to\nyour palette, or putting it in your own logo is not. Quoting \"it's CC0\" as\nclearance is the wrong answer.</p>\n<p><strong>A company logo is not a UI icon.</strong> Three sources cover brand marks and they\ntrade off reach against commitment — <strong>Simple Icons</strong> (committed, monochrome,\nthemeable), <strong>theSVG</strong> (MIT, 6,500+ marks in brand colour via\n<code>npm i thesvg</code>, an MCP server needing no key, or <code>npx skills add glincker/thesvg</code>), and <strong>Brandfetch</strong> (runtime lookup by <em>domain</em>, any company, nothing\ncommitted; free key at developers.brandfetch.com/dashboard, hotlink-only URLs\nthat expire in ~24h, and two free tiers that differ by 10,000x). All three carry\nthe identical trademark position — see\n<a href=\"references/icon-sources.md#brand-marks--three-sources-one-trademark-position\">Brand marks</a>.\nThey resolve the mark for you; none of them clears it.</p>\n<p><strong>Aggregators hide the licence.</strong> Iconify, and any icon-search MCP or plugin,\nresolve across 150+ sets each keeping its own terms. Record the <em>originating\nset</em> and its licence when the icon enters the repo — one line in <code>LICENSES.md</code>\nor atop the sprite. That single line is the difference between an answerable\nquestion and an audit.</p>\n<h3>3. Normalize before it enters the repo</h3>\n<p>Vendor output is not shippable. <code>scripts/normalize-icon.py</code> strips editor cruft,\n<strong>namespaces internal ids</strong> so two inlined SVGs cannot clobber each other's\ngradients, drops fixed <code>width</code>/<code>height</code> so CSS controls size, and applies the\ncorrect accessibility attributes.</p>\n<p><strong>Colour is never guessed.</strong> A single-colour source rebinds to <code>currentColor</code>. A\nmulti-colour source is <strong>refused (exit 11)</strong> until you name the treatment,\nbecause flattening a mark to a silhouette is lossy <em>and</em> counts as modifying it:</p>\n<table>\n<thead>\n<tr>\n<th>Flag</th>\n<th>Result</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><em>(default)</em></td>\n<td>mono source → <code>currentColor</code></td>\n</tr>\n<tr>\n<td><code>--keep-colour</code></td>\n<td>colours untouched — the right default for someone else's mark</td>\n</tr>\n<tr>\n<td><code>--greyscale</code></td>\n<td>Rec.709 luminance-mapped grey</td>\n</tr>\n<tr>\n<td><code>--tint '#fff'</code></td>\n<td>flatten to one colour; white = knockout / reverse-out</td>\n</tr>\n<tr>\n<td><code>--flatten</code></td>\n<td>yes, really collapse a multi-colour source to <code>currentColor</code></td>\n</tr>\n</tbody>\n</table>\n<p><strong>It also sanitises.</strong> An <em>inlined</em> SVG runs script in your page's origin; an\n<code>&lt;img src=\"x.svg\"&gt;</code> does not. Since this skill tells you to inline third-party\nSVGs, the normalizer strips <code>&lt;script&gt;</code>, <code>&lt;foreignObject&gt;</code>, every <code>on*</code> handler\nand <code>javascript:</code>/<code>data:text</code> hrefs. Treat any SVG you did not author as\nuntrusted input, and never inline one that has not been through this.</p>\n<pre><code># Would this file change? exit 10 = yes, 0 = already clean\nscripts/normalize-icon.py --check vendor.svg\n\n# Normalize a filled icon into the repo (atomic write)\nscripts/normalize-icon.py vendor.svg -o src/icons/search.svg\n\n# A brand mark: keep its colours, just clean and namespace it\nscripts/normalize-icon.py --keep-colour acme.svg -o src/logos/acme.svg\n\n# Stroke icon: forces fill=none, stroke=currentColor, consistent caps/joins\nscripts/normalize-icon.py --stroke vendor.svg -o src/icons/search.svg\n\n# Append to a sprite as a &lt;symbol&gt;\nscripts/normalize-icon.py --symbol --id i-search vendor.svg &gt;&gt; src/sprite.svg\n\n# Machine-readable result (what changed, and why)\nscripts/normalize-icon.py --json vendor.svg | jq '.data[0]'\n</code></pre>\n<p>Exit codes: <code>0</code> ok · <code>2</code> usage · <code>3</code> no such file · <code>4</code> not a usable SVG ·\n<code>10</code> (<code>--check</code> only) normalization would change the file · <code>11</code> multi-colour\nsource refused. The <code>--check</code> mode is a CI gate — run it over <code>src/icons/</code> to\nkeep un-normalized icons out.</p>\n<p>For byte-level path optimisation, run <strong>SVGO after</strong> normalizing, never before:</p>\n<pre><code>scripts/normalize-icon.py raw.svg -o icon.svg &amp;&amp; npx svgo --multipass icon.svg\n</code></pre>\n<h3>4. Deliver</h3>\n<table>\n<thead>\n<tr>\n<th>Mechanism</th>\n<th>Themeable</th>\n<th>Use when</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong><code>&lt;symbol&gt;</code> sprite + <code>&lt;use&gt;</code></strong></td>\n<td>Yes</td>\n<td><strong>Default for a real UI</strong> — many icons, reused</td>\n</tr>\n<tr>\n<td>Inline <code>&lt;svg&gt;</code></td>\n<td>Yes</td>\n<td>Few icons, or per-path styling/animation</td>\n</tr>\n<tr>\n<td>Framework component</td>\n<td>Yes</td>\n<td>Component stack already in play; tree-shakes</td>\n</tr>\n<tr>\n<td><code>&lt;img src=\"icon.svg\"&gt;</code></td>\n<td><strong>No</strong></td>\n<td>Never for UI icons</td>\n</tr>\n<tr>\n<td>Icon font</td>\n<td>Colour only</td>\n<td>Legacy only — migrate, don't extend</td>\n</tr>\n</tbody>\n</table>\n<p>Start a sprite from <a href=\"assets/sprite-template.svg\"><code>assets/sprite-template.svg</code></a>,\nwhich carries the hiding pattern that survives Safari, per-symbol <code>viewBox</code> so\nmixed grids scale correctly, and the sizing rule.</p>\n<p><strong>Size in <code>em</code>, never <code>px</code>:</strong></p>\n<pre><code>.icon { width: 1em; height: 1em; flex: none; }\n</code></pre>\n<p><code>1em</code> keeps the icon optically matched to its label at every type scale.\n<code>flex: none</code> stops a flex parent squashing it into an ellipse — the most common\nicon layout bug there is.</p>\n<p><strong>The trap that only shows in production:</strong> an external\n<code>&lt;use href=\"/sprite.svg#id\"&gt;</code> is CORS-blocked cross-origin and renders nothing,\nsometimes with no console error. Inline the sprite into the document.</p>\n<p>Decision detail, icon-font failure modes, and optimisation order →\n<a href=\"references/inline-delivery.md\"><code>references/inline-delivery.md</code></a>.</p>\n<h3>5. Get the accessibility right — exactly two cases</h3>\n<p>Every icon is decorative or meaningful. Leaving it undecided is the defect.</p>\n<pre><code>&lt;!-- Decorative: text beside it already names the control --&gt;\n&lt;button&gt;\n  &lt;svg class=\"icon\" aria-hidden=\"true\" focusable=\"false\"&gt;&lt;use href=\"#i-trash\"/&gt;&lt;/svg&gt;\n  Delete\n&lt;/button&gt;\n\n&lt;!-- Meaningful: the icon IS the label --&gt;\n&lt;button aria-label=\"Delete item\"&gt;\n  &lt;svg class=\"icon\" aria-hidden=\"true\" focusable=\"false\"&gt;&lt;use href=\"#i-trash\"/&gt;&lt;/svg&gt;\n&lt;/button&gt;\n</code></pre>\n<p><strong>Name the control, not the icon.</strong> The counter-intuitive part is that the SVG\nstays <code>aria-hidden</code> in <em>both</em> cases — a name on the icon <em>and</em> on the button\nproduces a double announcement. <code>role=\"img\"</code> + <code>&lt;title&gt;</code> is for standalone\ngraphics, not for the contents of a control.</p>\n<p>Also: icon-only controls need a <strong>24×24 CSS px</strong> minimum interactive area (WCAG\n2.2 §2.5.8) — pad the control, don't grow the glyph. Never let colour alone carry\nmeaning: pair it with a distinct shape.</p>\n<h3>6. Variants and site icons</h3>\n<p>A mark rarely ships in one treatment. <strong>Use the owner's published mono/reversed/\ngreyscale asset when one exists</strong> — theirs is drawn, yours is computed, and a\ndesigner already fixed the hairline that vanishes when knocked out. Generate\nonly when they publish none.</p>\n<pre><code>scripts/normalize-icon.py --tint '#fff'  acme.svg -o src/logos/acme-knockout.svg\nscripts/normalize-icon.py --greyscale    acme.svg -o src/logos/acme-grey.svg\n</code></pre>\n<p><code>filter: grayscale(1)</code> is right for a <em>hover-reveal effect</em> and wrong for a\ncanonical asset. <code>filter: invert(1)</code> is <strong>never</strong> a knockout — it inverts hue\ntoo, so a blue mark comes back orange.</p>\n<p><strong>Logo walls: constrain both axes.</strong> <code>width: 120px</code> on everything makes a wide\nwordmark occupy ~3x the visual area of a square badge. Use <code>max-width</code> <strong>and</strong>\n<code>max-height</code> in a fixed box, then correct optically by eye.</p>\n<p><strong>Favicons are a different mark</strong>, not your logo scaled down — four files\n(<code>favicon.ico</code>, <code>icon.svg</code>, <code>apple-touch-icon.png</code> 180x180, and a <em>separate</em>\n512x512 maskable PNG whose content sits inside the centre 80%-diameter circle).</p>\n<p>Variant production, light/dark pairs, logo-wall sizing and logo <code>alt</code> conventions\n→ <a href=\"references/brand-variants.md\"><code>references/brand-variants.md</code></a>. The favicon set,\nthe theme-aware SVG favicon, and maskable safe zones →\n<a href=\"references/favicons-and-app-icons.md\"><code>references/favicons-and-app-icons.md</code></a>.</p>\n<h2>What this skill doesn't cover</h2>\n<ul>\n<li><strong>Duotone/tri-tone treatments, filter-based tinting of a whole set, and\nraster→vector tracing</strong> → <code>svg-brand-tint-ops</code>. This skill produces flat\nvariants (mono, grey, knockout) of a single mark; that one does tonal\nre-mapping and vectorising.</li>\n<li><strong>Choosing the palette itself</strong> → <code>color-ops</code></li>\n<li><strong>Illustration and generative artwork</strong> → <code>genart-ops</code>, <code>isometric-ops</code></li>\n<li><strong>Authoring new icons</strong> — this skill sources, vets and ships existing ones</li>\n</ul>\n<h2>Cross-references</h2>\n<table>\n<thead>\n<tr>\n<th>When</th>\n<th>Use</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>The icons are right but the palette isn't</td>\n<td><code>color-ops</code></td>\n</tr>\n<tr>\n<td>A whole set needs brand recolouring or a logo needs vectorising</td>\n<td><code>svg-brand-tint-ops</code></td>\n</tr>\n<tr>\n<td>Building the surrounding component styles</td>\n<td><code>tailwind-ops</code></td>\n</tr>\n</tbody>\n</table>\n<h2>References</h2>\n<ul>\n<li><p><a href=\"references/icon-sources.md\"><code>references/icon-sources.md</code></a> — the set comparison\ntable (licence, grid, family, notes) for the eleven sets worth knowing; the\ntrademark-vs-file-licence distinction for brand marks; the aggregator licence\ntrap; MCP/plugin sourcing discipline; what each licence class actually\nrequires by way of attribution; and <strong>brand marks</strong> — Simple Icons vs theSVG\nvs Brandfetch compared on shape, coverage, colour and offline behaviour, plus\nBrandfetch's key setup, its two very different free tiers, hotlink/expiry\nconstraints, and both MCP servers. Load when choosing a set, sourcing a\ncompany logo, or before shipping any brand mark.</p>\n</li>\n<li><p><a href=\"references/inline-delivery.md\"><code>references/inline-delivery.md</code></a> — delivery\nmechanism comparison and why icon fonts fail; the external-<code>&lt;use&gt;</code> CORS trap;\n<code>em</code> sizing and optical alignment; <code>currentColor</code> theming; the full\naccessibility checklist (both cases, target size, contrast, reduced motion);\nand SVGO ordering. Load when wiring icons into a page or debugging one that\nwon't theme.</p>\n</li>\n<li><p><a href=\"references/brand-variants.md\"><code>references/brand-variants.md</code></a> — producing mono,\ngreyscale, knockout and single-ink variants of a mark; why Rec.709 luminance\nbeats an RGB average; when a CSS filter is right and when it is a lie; the three\nlight/dark approaches and why the internal-media-query one usually breaks;\nlogo-wall sizing by area rather than width; and logo <code>alt</code> conventions. Load\nwhen a mark needs a treatment it did not ship with.</p>\n</li>\n<li><p><a href=\"references/favicons-and-app-icons.md\"><code>references/favicons-and-app-icons.md</code></a> —\nthe modern four-file set and the head block that serves it, why <code>rel=\"shortcut icon\"</code> is meaningless, the theme-aware SVG favicon, Android maskable safe zones,\nand designing a mark down to 16px. Load for favicons, PWA icons or app icons.</p>\n</li>\n</ul>\n<h2>Scripts</h2>\n<ul>\n<li><code>scripts/normalize-icon.py</code> — normalize a vendor SVG for inline themeable use.\n<code>--check</code> for a CI gate, <code>--stroke</code> for stroke families, <code>--symbol --id</code> for\nsprite assembly, <code>--json</code> for a machine-readable diff summary. Idempotent:\nre-running on a normalized file reports clean.</li>\n</ul>\n<h2>Assets</h2>\n<ul>\n<li><code>assets/sprite-template.svg</code> — commented <code>&lt;symbol&gt;</code> sprite scaffold to copy\ninto a project, carrying the Safari-safe hiding pattern, per-symbol <code>viewBox</code>,\n<code>currentColor</code> defaults, and both filled and stroke examples.</li>\n</ul>\n","files":[{"path":"assets/sprite-template.svg","sizeBytes":2770,"isText":false},{"path":"references/brand-variants.md","sizeBytes":8223,"isText":true},{"path":"references/favicons-and-app-icons.md","sizeBytes":5568,"isText":true},{"path":"references/icon-sources.md","sizeBytes":11039,"isText":true},{"path":"references/inline-delivery.md","sizeBytes":6993,"isText":true},{"path":"scripts/normalize-icon.py","sizeBytes":21754,"isText":true},{"path":"SKILL.md","sizeBytes":15448,"isText":true},{"path":"tests/run.sh","sizeBytes":14285,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-30T19:37:46.871213Z","sha256":"01824CDD456D081A3720B9DEB77FBAED7ACB633DAD564EB046E6315B55D0939E","sizeBytes":35915},"review":null,"source":{"repositoryUrl":"https://github.com/0xDarkMatter/claude-mods","path":"skills/icon-ops","license":"MIT","commit":"3dfaf0ba5753026a99ee13f9d9ed56b9793bb6e8","subtreeSha":"BB975E4C90F5CCFC6737D9124006F719F51E1015007E706CB7CFC8F33491F0CF","lastSyncedAt":"2026-09-30T19:37:28.226022Z"},"reviewedAt":"2026-09-30T19:39:38.010874Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/icon-ops"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart"},{"target":"git","command":"git clone https://github.com/0xDarkMatter/claude-mods.git"}]}