Using Heroicons, Phosphor and Inline SVG Correctly
Icons are omnipresent in every UI, and yet they are frequently used incorrectly. Too large, wrong color, not accessible, shipped as PNG instead of SVG. This guide shows how to use Heroicons, Phosphor Icons and inline SVG with Tailwind CSS v4 correctly and performantly.
Table of Contents
- 1. Why SVG icons and not an icon font
- 2. Heroicons: the natural choice for Tailwind projects
- 3. Phosphor Icons: flexibility through multiple styles
- 4. Controlling size and color with Tailwind
- 5. Accessibility: aria-hidden, aria-label and title
- 6. Inline SVG: when and how
- 7. SVG sprites for scalable icon systems
- 8. Hover and transition animations on icons
- 9. Icon approaches compared
- 10. Summary
- 11. FAQ
1. Why SVG icons and not an icon font
Icon fonts like Font Awesome were the standard for a long time, but they have structural downsides that clash with the modern frontend approach of Tailwind CSS. Icon fonts always load the entire font file, even when only five icons are used. They do not scale sharply on every display type (especially retina displays). They block rendering because they load as a font. And they are harder to make accessible: an icon font glyph is an unknown character sequence for a screen reader unless it is manually patched up with aria-hidden and labels.
SVG icons solve all of these problems. As inline SVG directly in the HTML, they are available without an extra request, scale losslessly to any pixel, and can be controlled in size and color through Tailwind classes like w-5 h-5 text-sky-600. The color of an inline SVG icon with fill="currentColor" or stroke="currentColor" inherits the CSS color property of the parent element, so Tailwind's text-{color} classes act directly on the icon. That makes Tailwind Icons a feature fully integrated into the utility system.
2. Heroicons: the natural choice for Tailwind projects
Heroicons was built by the same developers as Tailwind CSS (Adam Wathan, Steve Schoger), so it is aligned with the Tailwind ecosystem both by name and concept. The icon library offers over 300 icons in three styles: outline (2px stroke, no fill), solid (fill, no stroke), and mini (16px, more compact for tightly packed UI elements). For Tailwind projects, the outline variant is the default for navigation, buttons and forms. The solid variant fits decorative elements and active states.
In Hyva-Magento projects, Heroicons are used directly as inline SVG in .phtml templates. The icon SVG is copied from heroicons.com, pasted into the template, and given Tailwind classes (class="w-5 h-5"). Alternatively, for component-based usage you can build PHP helper methods that resolve icons by name and return them as an SVG string. In Tailwind CSS projects with a build step (e.g. Vite + Vue), the NPM package @heroicons/vue or @heroicons/react is the fastest integration; in a Hyva context, inline SVG is the standard solution.
<!-- Heroicons inline SVG examples with Tailwind sizing and color -->
<!-- Outline style: 24px, stroke-based, inherits text color -->
<button class="flex items-center gap-2 text-slate-700 hover:text-sky-600 transition-colors font-medium text-sm">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 15.803 7.5 7.5 0 0016.803 15.803z"/>
</svg>
Search
</button>
<!-- Solid style: fill-based, great for filled states -->
<span class="flex items-center gap-1.5 text-emerald-600 text-sm font-semibold">
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd"/>
</svg>
Verified
</span>
<!-- Mini style (16px): compact, for badges and tight UIs -->
<span class="inline-flex items-center gap-1 bg-sky-100 text-sky-700 px-2.5 py-0.5 rounded-full text-xs font-semibold">
<svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8z"/>
<path d="M6.5 7.75A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75z"/>
<path d="M8 6a1 1 0 100-2 1 1 0 000 2z"/>
</svg>
Info
</span>
<!-- Icon-only button: aria-label required for screen readers -->
<button
aria-label="Open notifications"
class="relative p-2 text-slate-500 hover:text-slate-800 hover:bg-slate-100 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"/>
</svg>
<!-- Notification badge -->
<span class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" aria-hidden="true"></span>
</button>
3. Phosphor Icons: flexibility through multiple styles
Phosphor Icons is a younger, very extensive icon library with over 1,200 icons in six styles: Regular, Bold, Thin, Light, Fill and Duotone. The Duotone style is Phosphor's signature feature: it combines two color tones within a single icon and produces a striking effect that can be controlled with Tailwind's text-{color} and fill attributes. For projects that need a richer icon language or want to deviate from the cleaner Heroicons style, Phosphor is the most recommendable alternative.
Phosphor Icons can be integrated into Tailwind projects as an NPM package (@phosphor-icons/web) with web component syntax (<ph-icon icon="arrow-right" />) or as inline SVG from phosphoricons.com. For Hyva-Magento projects that do not use a JavaScript bundle, inline SVG is the correct choice. Phosphor's SVG paths follow a consistent 256x256 viewbox; you adjust size and color through Tailwind classes just like Heroicons. The Bold style is especially legible for small icons on light backgrounds.
4. Controlling size and color with Tailwind
The most elegant feature of Tailwind Icons is their seamless integration into the utility system for size and color. Size is controlled with w-{n} h-{n}: w-4 h-4 for 16px, w-5 h-5 for 20px, w-6 h-6 for 24px. These are the three most common icon sizes in UI systems. For display icons in empty states or illustrations, use w-12 h-12 or w-16 h-16. The square shape with equal width and height keeps the aspect ratio correct at all times.
Color is controlled through text-{color} on the SVG element or on a parent element, provided the SVG uses fill="currentColor" (for fill-based icons) or stroke="currentColor" (for stroke-based icons like Heroicons Outline). The SVG then inherits the CSS color value. This enables color cascades: a button with text-slate-700 hover:text-sky-600 automatically changes the icon color on hover, without needing hover: classes directly on the SVG. For duotone icons (Phosphor), you can control the second color through opacity attributes on specific paths.
<!-- Icon size and color control with Tailwind utilities -->
<!-- Size scale: 4 -> 5 -> 6 -> 8 -> 12 -->
<div class="flex items-end gap-4 text-sky-600">
<!-- 16px: for badges, compact UI elements -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
</svg>
<!-- 20px: standard button icon -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
</svg>
<!-- 24px: navigation, feature icons -->
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
</svg>
<!-- 48px: empty states, feature illustrations -->
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
</svg>
</div>
<!-- Color cascade: parent color propagates to child SVG -->
<button class="flex items-center gap-2 text-slate-600 hover:text-sky-600 transition-colors text-sm font-medium">
<!-- Icon automatically changes color on parent hover via currentColor -->
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd"/>
</svg>
Add to favorites
</button>
5. Accessibility: aria-hidden, aria-label and title
Accessibility for Tailwind Icons follows two clear rules. First: decorative icons that sit next to visible text get aria-hidden="true". Screen readers ignore the icon and read only the text, which is the complete description. This prevents duplicate output such as "search icon search" instead of just "search". Second: icons that stand alone and perform a function (icon-only buttons) need a text label for screen readers. That can be either an aria-label on the button or a title element as the first child of the SVG.
The decision rule: does the interactive element have a visible text label? Then use aria-hidden="true" on the SVG. No visible label? Then use aria-label="description" on the button or link, and aria-hidden="true" on the SVG. The <title> element directly inside the SVG is the W3C-conformant method, but is interpreted inconsistently by different screen readers; aria-label on the interactive element is more consistent. For purely decorative, non-interactive icons (ornamental icons in feature sections), aria-hidden="true" alone is always sufficient and correct.
6. Inline SVG: when and how
Inline SVG is the standard method for Tailwind Icons in projects without a JavaScript build pipeline, that is, in classic PHP/Magento projects with Hyva Themes. The advantage: no HTTP request, no render blocking, full color control via currentColor. The downside: the HTML gets larger, and the same icon SVG must be repeated in the HTML for every use. For icons that only appear once or twice on a page, this is not a problem. For icons that repeat a hundred times on a product listing page (star ratings, cart icons), the SVG sprite technique is the better choice.
When embedding inline SVG, always remove the irrelevant attributes: xmlns is optional in an HTML5 context, width and height attributes are overridden by Tailwind classes and can be dropped, and id attributes should not be copied over from the library if multiple icons on a page would end up sharing the same IDs. A cleaned-up SVG from Heroicons looks like this in Tailwind: <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> with only the relevant path element inside.
7. SVG sprites for scalable icon systems
SVG sprites solve the repetition problem of inline SVG: all icons are gathered in a single SVG sprite file that is loaded and cached once. Each icon is a <symbol> element with a unique id. At the point of use, you reference the icon with <use href="/icons/sprite.svg#icon-name">. The HTML stays compact, and the icon can be used as often as needed without code duplication.
In Tailwind projects, you shape the icon call as a minimal inline SVG wrapper element: <svg class="w-5 h-5 text-sky-600" aria-hidden="true"><use href="/icons/sprite.svg#search"></use></svg>. Size and color are still controlled through Tailwind classes on the outer SVG element. The <use> approach has one downside: external SVG sprite files cannot be manipulated via CSS currentColor when the sprite file is loaded through a separate HTTP request (CORS restrictions). This can be solved with an inline sprite at the top of the page (the whole sprite once, as a hidden SVG in the body), after which currentColor works again.
<!-- SVG Sprite: hidden sprite definition at top of page body -->
<svg class="sr-only" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- search icon symbol -->
<symbol id="icon-search" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 15.803 7.5 7.5 0 0016.803 15.803z"/>
</symbol>
<!-- heart icon symbol -->
<symbol id="icon-heart" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/>
</symbol>
<!-- star icon symbol -->
<symbol id="icon-star" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"/>
</symbol>
</defs>
</svg>
<!-- Usage: use href="#id", inherits currentColor from parent svg element -->
<button aria-label="Search" class="p-2 text-slate-500 hover:text-sky-600 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" aria-hidden="true">
<use href="#icon-search"/>
</svg>
</button>
<!-- Star rating with repeated icon, no duplication of SVG path -->
<div class="flex gap-0.5 text-amber-400" aria-label="Rating: 4 out of 5 stars">
<svg class="w-4 h-4" fill="currentColor" aria-hidden="true"><use href="#icon-star"/></svg>
<svg class="w-4 h-4" fill="currentColor" aria-hidden="true"><use href="#icon-star"/></svg>
<svg class="w-4 h-4" fill="currentColor" aria-hidden="true"><use href="#icon-star"/></svg>
<svg class="w-4 h-4" fill="currentColor" aria-hidden="true"><use href="#icon-star"/></svg>
<svg class="w-4 h-4" fill="none" stroke="currentColor" aria-hidden="true"><use href="#icon-star"/></svg>
</div>
8. Hover and transition animations on icons
Animated icons make interfaces feel more alive and give users precise feedback on interactions. With Tailwind CSS, simple icon animations can be built entirely without JavaScript. The basic pattern: transition-transform duration-200 on the SVG element, and a hover transform via hover:translate-x-1 (arrow icons pointing right slide further right), hover:scale-110 (icons that grow slightly on hover) or hover:rotate-12 (settings icons that rotate).
More complex animations, such as a star that fills in on click, need a state switch, which you can implement with the peer-checked pattern (for form checkboxes) or Alpine.js (x-data, :class). The wishlist heart icon is a classic example: outline heart initially, solid heart once added. With Alpine.js and Tailwind's hidden/block toggle, this is implemented in a few lines. The SVG itself needs no special class; Tailwind Icons are fully compatible with Alpine.js state management.
<!-- Animated icons with Tailwind transitions and Alpine.js state -->
<!-- Arrow icon: slides right on hover (parent hover, via group) -->
<a href="/more" class="group inline-flex items-center gap-2 text-sky-600 font-semibold text-sm hover:text-sky-700">
Learn more
<svg class="w-4 h-4 transition-transform duration-200 group-hover:translate-x-1"
fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
</svg>
</a>
<!-- Settings icon: rotates 90deg on hover -->
<button class="p-2 text-slate-400 hover:text-slate-700 transition-colors rounded-lg hover:bg-slate-100">
<svg class="w-5 h-5 transition-transform duration-300 hover:rotate-90"
fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-label="Settings">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</button>
<!-- Wishlist heart: toggle with Alpine.js state -->
<div x-data="{ liked: false }">
<button
@click="liked = !liked"
:aria-label="liked ? 'Remove from wishlist' : 'Add to wishlist'"
:class="liked ? 'text-red-500 hover:text-red-600' : 'text-slate-400 hover:text-red-500'"
class="p-2 transition-colors duration-200 rounded-lg">
<!-- Outline heart: visible when not liked -->
<svg x-show="!liked" class="w-5 h-5 transition-transform duration-200 hover:scale-110"
fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/>
</svg>
<!-- Solid heart: visible when liked -->
<svg x-show="liked" x-cloak class="w-5 h-5"
fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z"/>
</svg>
</button>
</div>
9. Icon approaches compared
The choice of the right icon approach in a Tailwind project depends on project size, build pipeline, and the ratio of unique to repeated icons.
| Approach | Color control via Tailwind | HTTP requests | Best suited for |
|---|---|---|---|
| Inline SVG (Heroicons) | Full via currentColor | 0 (in HTML) | Standard for most Tailwind projects |
| Inline sprite (body) | Full via currentColor | 0 (in HTML) | Many repeats of the same icon |
| External SVG sprite | Limited (CORS) | 1 cached request | Large icon libraries, when color is fixed |
| Icon font (Font Awesome) | Limited (font rendering) | 1+ (font files) | Legacy projects, migration needed |
| PNG/JPEG icon | None | 1 per icon | Only for complex illustrations, not for UI icons |
For new Tailwind projects with Hyva Themes, inline SVG with Heroicons is the clear recommendation: no build step needed, full color control, zero extra HTTP requests. For product listing pages with star ratings (appearing on every product card), the inline sprite is the more efficient solution. Icon fonts should not be used in new projects; the performance and accessibility downsides outweigh any convenience benefit.
Mironsoft
Tailwind CSS · SVG Icons · Hyva Themes · Accessibility
Need an icon system for your Tailwind project?
We build consistent, accessible icon systems with Heroicons and SVG sprites, integrated into Tailwind CSS v4 and Hyva Themes, with full keyboard and screen reader support.
Icon audit
Analyze existing icon fonts, review accessibility and create a migration plan
Sprite build-out
Set up an SVG sprite system with Heroicons or Phosphor for your project
Hyva integration
PHP helper methods for icon output in Magento phtml templates
10. Summary
SVG icons with Tailwind CSS are the only sensible approach for modern web projects: no font files, no render-blocking resources, full color control via currentColor and text-{color} classes. Heroicons is the natural complement for Tailwind projects: a consistent style, three variants (Outline, Solid, Mini) and the same design values as Tailwind itself. Phosphor Icons extends the possibilities for projects that need duotone icons or other styles.
The accessibility rules are simple: aria-hidden="true" for decorative icons next to text, aria-label on the interactive element for icon-only buttons. For pages with many repetitions of the same icon, the inline sprite is the most performant solution, without HTTP request overhead and without CORS restrictions on color control. Animations, hover transforms, state switches with Alpine.js, are all fully achievable with Tailwind transitions and Alpine.js x-show/:class.
Tailwind Icons, the essentials at a glance
Color control
SVG with fill="currentColor" or stroke="currentColor". Then text-{color} on the SVG or parent element controls the icon color, including hover: cascades.
Accessibility
Decorative next to text: aria-hidden="true". Icon-only button: aria-label="..." on the button, aria-hidden="true" on the SVG.
Sprite vs. inline
Inline SVG for one-off icons. Inline sprite in the body for icons that occur multiple times on a page (stars, cart icons in listings).
Size standards
w-4 h-4 (16px, badges), w-5 h-5 (20px, buttons), w-6 h-6 (24px, navigation), w-12 h-12 (48px, empty states). Always set w and h equal.