How to how to build a tailwind-first svg icon library
- Step 1Design every icon in one colour — In Figma/Illustrator, draw each icon with a single colour for all fills and strokes. A monochrome source is exactly what the
currentconversion expects — multi-colour artwork will flatten, so reserve colour for icons you'll theme with variables instead. - Step 2Normalise the viewBox, drop fixed dimensions — Give every icon a consistent
viewBox(e.g.0 0 24 24) and remove anywidth/heightattributes from the root<svg>. Fixed dimensions block Tailwind'sw-*/h-*utilities from sizing the icon. - Step 3Convert each icon to currentColor — Run every file through svg-to-tailwind. The default mode replaces all coloured fills/strokes (and inline-style colours) with
currentColorand addsclass="fill-current"to the root. For a whole folder, loop the paired local runner. - Step 4Decide on a sizing default — If you wrap icons in components, apply a default like
class="w-6 h-6"that consumers can override. Never re-introduce fixedwidth/heighton the SVG — keep sizing in utilities. - Step 5Wire dark mode at the wrapper — Because colour follows
color, you theme the whole library withtext-*/dark:text-*on parents — no per-icon dark variants. Document this as the colour contract for consumers. - Step 6Wrap as components and publish — Feed the themeable SVGs to svg-to-jsx or svg-to-vue-svelte (both default
colortocurrentColor), then publish the set to npm or your monorepo.
The icon-library pipeline
Each stage and the JAD tool that performs it. svg-to-tailwind is the colour step; sizing is a markup convention, not a tool option here.
| Stage | What you do | Tool |
|---|---|---|
| Design | One colour per icon; consistent viewBox | Figma/Illustrator |
| Strip dimensions | Remove fixed width/height; fix the viewBox | Hand-edit or design export |
| Theme colour | All colours → currentColor + fill-current | svg-to-tailwind |
| Optimise | Drop metadata, minify path data | svg-pro-minifier |
| Componentise | Wrap as React/Vue/Svelte with currentColor default | svg-to-jsx, svg-to-vue-svelte |
Which Tailwind utilities drive a converted icon
After the currentColor conversion, these utilities on the icon or its parent control appearance with no SVG edits.
| Goal | Utility | Requirement |
|---|---|---|
| Colour | text-gray-600 (parent) | fill-current + fill="currentColor" present |
| Dark-mode colour | dark:text-white (parent) | Same as above; Tailwind dark mode enabled |
| Size | w-5 h-5 | viewBox set, no fixed width/height |
| Stroke colour (outline icons) | stroke-current / text-* | stroke="currentColor", fill="none" kept |
| Opacity / transform | opacity-*, rotate-*, scale-* | Applied to the <svg> element |
Cookbook
Conventions and the converted markup that makes a Tailwind-first set work.
A converted, library-ready icon
Single-colour source after svg-to-tailwind: currentColor fills, fill-current on root, viewBox set, no fixed dimensions. This is the canonical shape for the set.
<svg class="fill-current" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path d="M21 21l-4.3-4.3M11 19a8 8 0 100-16 8 8 0 000 16z"
fill="currentColor"/>
</svg>
Consumer:
<SearchIcon class="w-5 h-5 text-gray-600 dark:text-white" />Outline icon set
Stroke-only icons keep fill='none' and get stroke='currentColor'. Pair fill-current with stroke inheritance so both fills (none) and strokes behave.
<svg class="fill-current" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
Consumer:
<ArrowIcon class="w-6 h-6 text-blue-500" />Component wrapper with a default size
Give a default w-6 h-6 the consumer can override; never set width/height on the SVG itself.
export function Icon({ className = '', children }) {
return (
<span className={`inline-block w-6 h-6 ${className}`}>
{children}
</span>
);
}
// <Icon className="w-4 h-4 text-gray-400"><SearchIcon/></Icon>Where the monochrome model ends
A two-tone brand mark can't survive the current-mode flatten. Use CSS variables for the parts that must theme independently.
Brand mark, two tones: body → var(--brand-icon, currentColor) accent→ var(--brand-accent) Produce with svg-css-variable-injector, not svg-to-tailwind. Keep the monochrome UI icons on currentColor/fill-current.
Disabled / state styling via the cascade
Because colour is just CSS color, state utilities on a parent flow into every icon for free.
<button class="group text-gray-700 disabled:text-gray-400"
disabled>
<TrashIcon class="w-5 h-5" /> <!-- greys out with the button -->
</button>Edge cases and what actually happens
Multi-colour brand icons flatten
By designcurrent mode maps every colour to one currentColor, so a two-tone logo loses its accent. That's correct for a monochrome UI set but wrong for brand marks. Keep brand icons on CSS variables (svg-css-variable-injector) and reserve svg-to-tailwind for the monochrome system icons.
Fixed width/height blocks Tailwind sizing
Watch closelyIf the root <svg> keeps width="24" height="24", w-5 h-5 won't resize it (the attributes win). svg-to-tailwind doesn't strip dimensions — it's a colour tool. Remove width/height during your design export or with a markup pass so viewBox-based sizing works.
No viewBox at all
Watch closelyWithout a viewBox, the SVG has no intrinsic aspect ratio to scale, so w-*/h-* produce inconsistent results. Add a viewBox to every icon as part of normalisation; svg-viewbox-fixer can compute one from the geometry.
Icon used via `<img>` ignores text-*
Not supportedAn icon loaded with <img src> or as a CSS background-image can't inherit the page's color, so text-* does nothing. A Tailwind-first set must be inlined (directly or via a component) to be themeable.
Outline icons lose their look if fill isn't none
Watch closelyOutline icons depend on fill="none". svg-to-tailwind preserves none, but if your source mistakenly has a solid fill, it'll become currentColor and the outline fills in. Verify outline icons render hollow after conversion.
Tailwind purges fill-current / text-* in the package build
Watch closelyWhen the library is consumed, the host app's Tailwind scans *its* source, not your package internals. Ensure fill-current and any default text-*/w-* you ship appear in the host's scanned files, or document a safelist for consumers.
Already-currentColor icons need no conversion
No-opIf you adopt an existing currentColor-based set (or icons exported that way), the current pass leaves colours unchanged and only ensures fill-current on the root. Don't re-run it expecting changes.
Mixed conversion across the set
Watch closelyIf some icons were converted and others weren't, consumers see inconsistent theming. Run the whole library through the same pipeline (loop the runner) so every icon obeys the same text-* contract.
Gradients in 'flat' icons
PreservedA supposedly monochrome icon that actually uses a subtle gradient won't theme — gradient fills and stop-color are left untouched. Either flatten the gradient in design or accept it stays fixed-colour in the otherwise-themeable set.
Frequently asked questions
How do icons respond to text colour with no prop?
After conversion every paint is currentColor and the root has fill-current. currentColor resolves to the inherited CSS color, so a parent text-gray-600 colours the icon. No color prop or per-icon CSS is needed.
How do I size icons in a Tailwind-first set?
With w-*/h-* utilities — but only if the icon has a viewBox and no fixed width/height on the root. svg-to-tailwind doesn't manage sizing; normalise the viewBox (use svg-viewbox-fixer if needed) and strip dimensions yourself.
What about multi-colour icons in my design system?
The current conversion flattens them to one colour, so they're a poor fit. Keep multi-tone brand marks on CSS variables via svg-css-variable-injector, and use svg-to-tailwind only for the monochrome UI icon set.
Do I get dark mode for free?
Yes for colour. Because the icon follows color, dark:text-white (or any dark:text-*) on a wrapper repaints the whole set. There are no per-icon dark variants to maintain.
Should each icon component set its own size?
Ship a sensible default like w-6 h-6 that consumers can override with their own w-*/h-*. Never bake width/height onto the SVG — that prevents utility overrides.
How do I convert the whole library at once?
Loop your folder through the paired local runner: each SVG → svg-to-tailwind with default current mode → write back. The runner runs the same engine as the website, so output matches the in-browser tool.
Will it ruin my outline (stroke-only) icons?
No. fill="none" is preserved, so outline icons stay hollow; only the stroke colour becomes currentColor. Add stroke-current if you want it explicit. Just confirm your sources use fill="none" and not a solid fill.
Can consumers use the icons outside Tailwind?
Partly. currentColor itself is plain CSS, so the icons follow color even without Tailwind — you just lose the fill-current/text-* utility ergonomics. For a fully framework-agnostic package, consider the CSS-variable scheme instead.
How do I turn these into React/Vue components?
Run the converted SVGs through svg-to-jsx or svg-to-vue-svelte. Both generate components whose color prop defaults to currentColor, so the text-* contract carries through to the component API.
Why do my icons render with no colour in production?
Almost always Tailwind purging fill-current or a dynamic text-*/fill-* class that isn't in the consumer's scanned source. Make those classes literal in the host app or document a safelist for the package.
Should I minify before or after conversion?
Either order works since the operations are independent, but minifying after conversion (svg-pro-minifier) ensures the final shipped bytes are smallest. Keep the pipeline order consistent across the library.
What plan do I need to run the conversion?
svg-to-tailwind requires the Pro plan. Building a library typically means many small files; per-job size caps (5 MB Free / 50 MB Pro / 2 GB Developer) won't be a factor for icon-scale SVGs.
Privacy first
Every JAD SVG tool runs entirely in your browser using the DOM API and Canvas. Your SVG files never leave your device — verified by zero outbound network requests during processing.