⌘K

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

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 prefix produces a flat name (brand-logo) — it is not the same as the prefix:name set 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, lucide aliases, 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'); // ok
app(IconManager::class)->registerIconSet(new LucideIconSet); // throws

The separator is a colon (:). Icon names themselves use dashes (arrow-down-tray), so there is no ambiguity. Use default:name to 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.

  • 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