Core
Icons
Where an icon name is resolved, how to add your own set, and what happens when two sets claim the same name.
On this page
- Icons
- Blade Usage
- PHP Usage
- Available Icons
- Wire-friendly Aliases
- Accessibility
- Adding Custom Icons
- 1. From a folder of SVG files (easiest)
- 2. Inline, by name
- 3. A reusable icon set (advanced)
- Using multiple icon sets
- The prefixes the packages already take
- Swapping the default (unprefixed) set
- Drawing a repeated icon once
- Catching typos
- Regenerating the bundled Heroicons
- Related
Every icon in the framework is a name, not markup: 'outline:user' travels
through a column, an action and a notification unchanged, and is turned into SVG
once, at render. That indirection is what lets an application swap the whole set
without touching a single component.
Icons
The complete Heroicons solid collection (324 icons,
20x20 viewBox) is bundled inline — no external dependencies, no extra package.
It is the default set, addressed with bare names (pencil, user). You can
register any number of additional sets (Lucide, Feather, your own brand icons)
alongside it — see Using multiple icon sets.
Each icon carries its own viewBox and fill/stroke styling, so 20×20 fill-based
Heroicons and 24×24 stroke-based sets render correctly side by side.
Blade Usage
<x-wire::icon name="check" class="w-5 h-5" /><x-wire::icon name="trash" class="w-4 h-4 text-red-500" /> {{-- A prefixed icon from another registered set --}}<x-wire::icon name="lucide:home" class="w-5 h-5" /> {{-- Expose to assistive tech (otherwise the icon is aria-hidden) --}}<x-wire::icon name="trash" label="Delete" />
PHP Usage
use NyonCode\WireCore\Foundation\Icons\IconManager; $manager = app(IconManager::class); $manager->render('check'); // full <svg> string$manager->render('trash', 'w-5 h-5', 'text-red-500', label: 'Delete');$manager->has('lucide:home'); // bool$manager->resolve('check'); // ?ResolvedIcon (body + viewBox + attrs)$manager->allNames(); // every available name (prefixed for non-default sets)
render() is the canonical entry point — it applies each icon's own viewBox and
styling. getPath() returns just the inner markup and is kept only for callers
that wrap their own <svg> (correct only for 0 0 20 20 fill icons).
Available Icons
Every default icon uses its canonical Heroicons name — the file name from heroicons.com (solid variant). Browse the full set there; a few examples:
academic-cap, arrow-down-tray, bars-3, chevron-up, cog-6-tooth,
document-text, envelope, funnel, magnifying-glass, pencil, qr-code,
trash, user, wrench-screwdriver, x-mark.
For IDE autocompletion you can reference icons through the Icon enum instead of
raw strings:
use NyonCode\WireCore\Foundation\Icons\Icon; Action::make('edit')->icon(Icon::pencilSquare);
Wire-friendly Aliases
A small set of short aliases map onto canonical icons for convenience:
| Alias | Resolves to | Alias | Resolves to |
|---|---|---|---|
pen, edit |
pencil |
delete |
trash |
view |
eye |
add |
plus |
download, export |
arrow-down-tray |
upload, import |
arrow-up-tray |
duplicate, copy |
document-duplicate |
x, close |
x-mark |
settings |
cog |
mail, email |
envelope |
exclamation, warning |
exclamation-triangle |
information, info |
information-circle |
question |
question-mark-circle |
archive |
archive-box |
refresh |
arrow-path |
shield |
shield-check |
lock |
lock-closed |
filter |
funnel |
more, dots-vertical |
ellipsis-vertical |
dots-horizontal |
ellipsis-horizontal |
external-link |
arrow-top-right-on-square |
Accessibility
Icons render as decorative by default (aria-hidden="true"). Pass a label when
the icon carries meaning on its own — it is then exposed as an image with that
label (role="img" + aria-label):
<x-wire::icon name="check-circle" label="Verified" />
Adding Custom Icons
You don't have to settle for the bundled set. Pick whichever approach fits. Custom
icons (folders and inline) are bare-named and take priority over the default
set, so a custom icon is used anywhere a name is accepted
(->icon('logo'), <x-wire::icon name="logo" />, …).
When you paste a complete <svg>…</svg>, its viewBox and styling attributes
(fill, stroke, stroke-width, …) are preserved — so you can drop in icons
from any source and format. A bare <path> fragment defaults to the Heroicons
solid format (0 0 20 20, fill="currentColor").
1. From a folder of SVG files (easiest)
Drop .svg files in a directory and register the path — the file name becomes the
icon name (logo.svg → logo). No class, no boilerplate.
Via config (config/wire-core.php), great for app-wide icons. A string key adds a
dash-joined name prefix and avoids file-name collisions between folders:
'icons' => [ 'paths' => [ resource_path('icons'), // resources/icons/logo.svg => "logo" 'brand' => resource_path('icons/brand'), // icons/brand/mark.svg => "brand-mark" ],],
Or at runtime:
use NyonCode\WireCore\Foundation\Icons\IconManager; app(IconManager::class)->registerIconsFromDirectory( resource_path('icons/brand'), prefix: 'brand', // brand/logo.svg => "brand-logo");
The folder
prefixproduces a flat name (brand-logo) — it is not the same as theprefix:nameset namespace described below.
2. Inline, by name
Register individual icons — paste a full <svg>…</svg> (the wrapper is stripped,
its viewBox/styling preserved) or just the inner <path>:
app(IconManager::class)->registerIcons([ 'logo' => '<svg viewBox="0 0 20 20"><path d="M10 2 …"/></svg>', 'spark' => '<path d="M10 1 12 8 …"/>',]);
Reuse the same name as a bundled icon to override it. Put the call in a service
provider's boot() so the icons are available everywhere:
public function boot(): void{ app(IconManager::class)->registerIconsFromDirectory(resource_path('icons'));}
3. A reusable icon set (advanced)
For a complete, swappable style, implement IconSet. Implement the optional
ProvidesIconMetadata capability too if your icons are stroke-based or use a
non-20x20 viewBox (Lucide, Feather, Heroicons outline) — that lets each icon
carry its own ResolvedIcon (body + viewBox + attributes):
use NyonCode\WireCore\Foundation\Icons\{IconSet, ProvidesIconMetadata, ResolvedIcon}; final class LucideIconSet implements IconSet, ProvidesIconMetadata{ private string $dir = '/abs/path/to/node_modules/lucide-static/icons'; public function getIcon(string $name): ?ResolvedIcon { $file = "{$this->dir}/{$name}.svg"; // fromSvg() keeps Lucide's viewBox="0 0 24 24" + fill=none stroke=currentColor. return is_file($file) ? ResolvedIcon::fromSvg(file_get_contents($file)) : null; } public function getPath(string $name): ?string { return $this->getIcon($name)?->body; } public function has(string $name): bool { return is_file("{$this->dir}/{$name}.svg"); } public function names(): array { /* basenames of *.svg */ return []; }}
Sets that implement only IconSet still work — their getPath() output is wrapped
in the default 0 0 20 20 fill format.
Using multiple icon sets
Resolution is deterministic and namespaced:
- The default set is unprefixed —
pencil,user,lucidealiases, custom icons — and is always Heroicons unless you swap it (below). - Every other set requires a unique prefix and is addressed as
prefix:name.
Register additional sets in config under their prefix key:
// config/wire-core.php'icons' => [ 'default_set' => 'default', 'sets' => [ 'default' => DefaultIconSet::class, // → "pencil" (Heroicons, 20×20 fill) 'lucide' => LucideIconSet::class, // → "lucide:home" (24×24 stroke) 'custom' => App\Wire\Icons\MyIconSet::class, ],],
<x-wire::icon name="pencil" /> {{-- Heroicons --}}<x-wire::icon name="lucide:home" /> {{-- Lucide --}}
This guarantees the sets never collide: a bare name is always the default set, a
prefixed name is always that exact set. Because of this, registering a non-default
set without a prefix throws an InvalidArgumentException:
app(IconManager::class)->registerIconSet(new LucideIconSet, 'lucide'); // okapp(IconManager::class)->registerIconSet(new LucideIconSet); // throws
The separator is a colon (
:). Icon names themselves use dashes (arrow-down-tray), so there is no ambiguity. Usedefault:nameto address the base set explicitly.
The prefixes the packages already take
Three sets are registered for you, by the packages that draw them. Each ships the glyphs Heroicons cannot answer for, in the format that one surface needs:
| Prefix | Registered by | Glyphs | Format |
|---|---|---|---|
wire |
wire-core | wire:star, wire:star-outline — the rating marks |
24×24 fill |
forms |
wire-forms | the rich editor's toolbar, 20 of them: forms:bold, forms:italic, forms:link, … |
24×24 fill |
table |
wire-table | table:checkbox-check, table:checkbox-indeterminate — the marks inside the table's own selection checkbox |
16×16 stroke |
The format is why a glyph belongs to a set rather than to a pile: a set declares one viewBox and one set of stroke/fill attributes for everything in it, so the table's checkbox marks — authored at 16×16 and stroked, because they are drawn inside a 16 px bordered box and nothing else — cannot share a set with 24×24 filled icons without one of the two rendering at the wrong weight. Bodies load on first use, so a page that draws none of a set's glyphs never reads its file.
Those three names are taken, and taking one back has an order to it.
Registering a set under a prefix that already has one replaces it silently,
and the packages register theirs while booting — after the manager has read
icons.sets from config, because that config is read when the manager is first
built. So declaring 'table' => MyIconSet::class in config does nothing: the
package's own registration runs later and wins. Register from your own service
provider's boot() instead, which runs after the packages':
public function boot(): void{ app(IconManager::class)->registerIconSet(new MyTableIconSet, 'table'); }
To change one glyph rather than all of them, let your set answer for that name and delegate the rest to the package's — a set is an object, so wrapping one is ordinary composition.
Swapping the default (unprefixed) set
To make a different set the unprefixed base — e.g. ship Lucide as your primary
style — point default_set at its key:
'icons' => [ 'default_set' => 'lucide', // bare names now resolve against Lucide 'sets' => [ 'lucide' => LucideIconSet::class, 'default' => DefaultIconSet::class, // left out: Lucide is the base now ],],
The shipped 'default' entry may stay in the list; it is skipped while another
set is the base, since default: always addresses the base set. A default_set
naming a key sets does not have throws an IconSetRegistrationException at
boot instead of quietly keeping Heroicons.
At runtime: app(IconManager::class)->setDefaultIconSet(new LucideIconSet).
Drawing a repeated icon once
A table draws the same few icons in every row — each action button, a boolean column, the selection mark — and every one of them carries its whole path data. Switch the sprite on and a repeated icon travels once:
// config/wire-core.php'icons' => [ 'sprite' => env('WIRE_ICONS_SPRITE', true), ],
Inside a Livewire render, each icon is the same <svg> it always was — the same
classes, viewBox, fill/stroke, aria-* and forwarded Alpine attributes — holding
<use href="#wi-…"/> instead of its body. The body goes out as a <symbol> once
per piece of markup Livewire sends whole:
| piece | carries the symbols it references |
|---|---|
| a component render | yes |
| an island render | yes |
a partial a write answers with (rowPartials(), form field partials) |
yes |
| anything outside a Livewire render — a PDF, a mail, plain Blade in a layout | not sprited at all; inline as before |
So whichever of them the browser morphs in, its icons resolve on their own,
before any script has run. The symbols ride inside the first sprited <svg>
of that piece rather than in a hidden element beside it: a new last child would
move every :last-child, last: and Tailwind 4 space-y-* in your markup.
A small script, wire-core-icons.js (under 1 kB), copies every symbol it sees
into one sprite at the end of <body>, which <use> falls back to when the
icon that carried a symbol has been morphed away — an island re-rendered around
it, say. It is handed to Livewire as an asset by the first component that draws
a sprited icon, so there is nothing to add to a layout.
Measured on a 50-row table with four icon columns, three row actions and a three-item group (605 icons, 17 distinct): 851 965 → 705 288 bytes raw (−17 %), 22 315 → 17 297 bytes gzipped (−22 %), render time unchanged.
What you trade. A <use> draws its symbol in a shadow tree, and selectors
do not cross into it. Inherited properties still reach the paths — color,
fill, stroke, opacity, stroke-dashoffset — which is everything the stack's
own markup relies on. A stylesheet of your own that reaches into an icon
(.toolbar svg path { stroke-width: 1 }) stops matching, which is why the switch
is off by default. Check your CSS for svg path-shaped selectors before turning
it on.
A few bodies are never sprited, whatever the switch says: one with ids or
url(#…) references (gradients, masks, clip paths), because every copy of the
symbol would carry the same id. Icons in a lazy action menu's spec stay inline
too — that JSON becomes markup only when the menu opens.
Rendering a sprited icon yourself needs nothing new: icon(), <x-wire::icon>
and IconManager::render() all decide per call. Code that builds the <svg>
itself asks IconManager::body($icon) for what goes inside it.
Catching typos
Set icons.warn_missing (or WIRE_ICONS_WARN_MISSING=true) to log a warning
whenever an unknown icon name is rendered — it still renders the fallback
placeholder, but the log helps surface typos in development.
Regenerating the bundled Heroicons
The bundled paths live in the generated PHP data file
packages/core/resources/icons/heroicons-solid.php, produced from the official
heroicons npm package (20/solid SVGs, keyed by file name). Regenerate that
file rather than editing icon paths by hand.
Related
- Colors — the same shape of resolver, for the other half of a surface
- Enums — an enum naming its own icon
- Theming — replacing the set an application uses
- Foundation — the concerns these are reached through