codeat3/blade-eos-icons
Blade EOS Icons brings the EOS open-source icon set to your Laravel Blade views, letting you drop crisp SVG icons into templates with simple components or directives and customize size, color, and class without extra markup or tooling.
Install, then verify — in that order. Installation is deliberately boring:
composer require codeat3/blade-eos-icons
No service provider to register (package discovery handles it), no assets to publish, no config to copy, no build step. The package is a blade-ui-kit/blade-icons driver: it registers one Blade component per SVG in the EOS set and delegates all rendering, attribute merging, and caching to blade-icons.
The first 10 minutes should go into confirming the component names, not building anything. The README shows <x-eos-icons::icon-name />, but the blade-* family of packages more commonly registers a flat, kebab-cased component per glyph. Don't trust either form — read the package:
ls vendor/codeat3/blade-eos-icons/resources/svg | head -50
sed -n '1,150p' vendor/codeat3/blade-eos-icons/src/*ServiceProvider.php
The SVG filename is the component name, including any variant suffix (-outlined, -filled, -round, -sharp, -two-tone). Then confirm it resolves:
<x-eos-icons-alert class="size-5 text-amber-500" aria-hidden="true" />
View-source on that render once. What you want to see:
<svg> (not an <img>, not a background image),fill="currentColor" (or similar) so text-* utilities colour it,class="size-5 text-amber-500" merged onto the root <svg> element,width/height that overrides your sizing utility.Everything downstream — sizing, colour, accessibility, dark mode — is determined by those four facts, so establish them before you write a single feature template.
First real use case: don't start with a hero banner. Take the 8–12 glyphs that appear in your nav, sidebar, table row actions, and primary buttons, and route them through one internal wrapper (below). That turns "we adopted an icon set" from an irreversible design decision into a one-file swap, which is the right stakes level for a single-maintainer package.
Version floor: ^1.16 if you're heading toward Laravel 13. The 1.16.0 release is constraint-widening only — no component renames, no API or config changes — so it's safe to land alongside other work. Just be aware the real Laravel 13 gate is whether the resolved blade-ui-kit/blade-icons in your lockfile declares support; check composer.lock, not this changelog.
This is the highest-leverage thing you will do with this package. No feature template should ever contain eos-icons. Put the mapping in one place:
{{-- resources/views/components/ui/icon.blade.php --}}
@props(['name', 'label' => null])
@php
$map = [
'alert' => 'eos-icons-alert',
'check' => 'eos-icons-check',
'archive' => 'eos-icons-archive-outlined',
'settings'=> 'eos-icons-settings',
];
$component = $map[$name] ?? null;
@endphp
@if ($component)
<x-dynamic-component
:component="$component"
{{ $attributes->merge(['class' => 'shrink-0']) }}
@if ($label) role="img" aria-label="{{ $label }}" @else aria-hidden="true" @endif
/>
@elseif (app()->hasDebugModeEnabled())
<span {{ $attributes }} title="Unknown icon: {{ $name }}">[{{ $name }}]</span>
@endif
If x-dynamic-component misbehaves with these registered components (it occasionally does with flattened, prefixed component names), swap the array for an explicit ladder. It's more verbose, but it's unambiguous and doubles as an auditable inventory of every glyph in the app:
@switch($name)
@case('alert') <x-eos-icons-alert {{ $attributes }} aria-hidden="true" /> @break
@case('check') <x-eos-icons-check {{ $attributes }} aria-hidden="true" /> @break
@default @if (app()->hasDebugModeEnabled()) <span {{ $attributes }}>[{{ $name }}]</span> @endif
@endswitch
The SVGs inherit currentColor, so Tailwind does the work: text-slate-500 dark:text-slate-400 colours, size-5 sizes. Set defaults in the wrapper's merge() so call sites stay terse:
<x-ui.icon name="alert" class="text-amber-500" />
<x-ui.icon name="check" label="Completed" />
If an icon stubbornly ignores your class, the upstream SVG has explicit width/height attributes — pass them explicitly (width="20" height="20") rather than fighting specificity.
The wrapper's $map lookup already rejects unknowns, but back it with an enum so the name set is typed and greppable:
enum Icon: string
{
case Alert = 'alert';
case Check = 'check';
}
<x-ui.icon :name="$row->status->icon()->value" />
Blade component names that resolve to nothing throw — which is better than a silent blank — but only if the template is actually rendered before production. Hence the next pattern.
Cheap, and it catches upstream renames, typos introduced by a mass find-and-replace, and Blade formatter damage:
public function test_every_mapped_icon_renders(): void
{
foreach (['eos-icons-alert', 'eos-icons-check', 'eos-icons-archive-outlined'] as $component) {
$html = Blade::render("<x-{$component} />");
$this->assertStringContainsString('<svg', $html, $component);
}
}
Better still, derive the list from the wrapper's map so the test can't drift from reality.
blade-icons caches icon discovery. If the command exists in your resolved version (php artisan list | grep icons to confirm), wire it into the deploy script after composer install and before view:cache:
php artisan icons:cache
php artisan view:cache
And the inverse after any composer update or package change:
php artisan icons:clear
php artisan view:clear
Icons work untouched inside Livewire components and inside <x-button> slots — they're just Blade components rendering markup. Two things to keep in mind:
<x-button><x-ui.icon name="check" /> Save</x-button> is cleaner than passing icon names into buttons and reassembling markup down there.Pin the version ("codeat3/blade-eos-icons": "^1.16"), consume only through the wrapper, and know what removal looks like: copying the 10–20 SVGs you actually use into resources/svg/ and pointing the wrapper's map at unprefixed names. With the wrapper in place that's an afternoon, not a migration — which is what justifies adopting a one-star, single-maintainer package at all.
ComponentNotFoundException is almost always a naming mismatch — check the vendor directory, not the README. The two most common failure modes are the wrong prefix (eos-icon- vs eos-icons-) and a missing variant suffix (eos-icons-archive doesn't exist; eos-icons-archive-outlined does). ls vendor/codeat3/blade-eos-icons/resources/svg settles it in two seconds.
Some glyph names are awkward. Names containing digits or unusual tokens can be fragile in Blade component parsing — verify any file whose name starts with a digit or contains a leading number renders before you add it to the map, and give it an alias in the wrapper if it doesn't.
Duplicate id attributes are a real risk. Many SVG sets embed <clipPath>/<mask> elements with internal ids. Render the same glyph twice on one page and you get duplicate IDs: broken masks on the second instance plus invalid HTML. Test by rendering your most-used icon twice in one view. Mitigations, in order of preference: use glyphs without internal IDs for repeated rows, wrap repeated blocks (the IDs are then scoped per-block only if you also namespace them), or switch to a <symbol>/sprite approach.
Payload grows linearly with icon density. Each instance inlines the full SVG. A dense table is the one place this package costs you anything measurable. If a screen has more than ~20 icons, memoise a rendered string in the wrapper or move to a sprite sheet — don't reach for a CDN, and don't assume HTTP caching will save you (the markup is in the HTML document).
Stale caches after composer update masquerade as breakage. Half of all "the icon disappeared" reports are a cached manifest. Run php artisan icons:clear and php artisan view:clear before you start bisecting.
There is nothing to publish. No config, no views in this package. If you need to change defaults (tag name, class merging behaviour, caching), you're editing blade-ui-kit/blade-icons config — and that affects every icon driver you have installed. Do it knowingly, and prefer the wrapper for anything app-specific.
Accessibility is your responsibility, and it belongs in the wrapper. Decorative icons need aria-hidden="true". Meaningful icons need role="img" + aria-label. Bake the branch into the wrapper so nobody has to remember it at the call site; a decorative icon that announces "graphic" to a screen reader is a bug you'll only find in an audit.
CSP is usually a non-issue — with one exception. Inline SVG is DOM markup, so a strict script-src/img-src doesn't touch it. But if a glyph ships an inline <style> block, a strict style-src will block it and you'll get a silently unstyled icon. Check your noisiest glyphs against your headers.
Blade formatters can mangle self-closing component tags. If Prettier or a Blade plugin is in the pipeline, verify <x-eos-icons-alert /> survives a format pass as self-closing and isn't expanded into <x-eos-icons-alert></x-eos-icons-alert> with stray whitespace — that whitespace shows up as layout shift inside flex containers.
Don't build an icon gallery page. The temptation is real and it costs a day. Document two things instead, in a paragraph: the wrapper's name convention, and the rule that feature templates never reference eos-icons directly. Spend the saved time adding entries to the map — that file is the only icon inventory that stays true.
How can I help you explore Laravel packages today?