driesvints/blade-icons
Blade Icons makes it easy to use any SVG icon set in your Laravel Blade views via components and directives like or @svg('camera'). Originally Blade SVG by Adam Wathan, it's the base for many third-party icon packages and supports custom icon sets.
1. Install and publish config
composer require blade-ui-kit/blade-icons
php artisan vendor:publish --tag=blade-icons
The published config/blade-icons.php ships with the default set commented out. Uncomment it and make sure the path exists:
'sets' => [
'default' => [
'path' => resource_path('svg'),
'prefix' => 'icon',
'class' => 'w-6 h-6', // optional default classes
],
],
2. Drop SVGs in resources/svg
A file at resources/svg/camera.svg is immediately available under two syntaxes:
{{-- Component syntax: <x-{prefix}-{file name} /> --}}
<x-icon-camera class="w-6 h-6 text-gray-500" />
{{-- Directive syntax: @svg($name, $class, $attributes) --}}
@svg('camera', 'w-6 h-6', ['fill' => 'none'])
3. Where to look first
config/blade-icons.php — sets, paths, prefixes, default classes. This is the whole mental model of the package.blade-ui-kit.com/blade-icons#search — to find which set package contains a given glyph before you build a custom one.First use case: replace hardcoded inline <svg> blobs in your layout/nav with <x-icon-* /> components so icon styling lives in Blade attributes (Tailwind classes) rather than duplicated markup. That single swap usually removes hundreds of lines of copy-pasted SVG and immediately makes icon swaps trivial.
Prefixes are the public API of your icons. Keep them short and stable, and let the prefix encode the source/weight so @svg() calls are self-documenting:
'sets' => [
'default' => ['path' => resource_path('svg'), 'prefix' => 'icon'],
'heroicon-o' => ['path' => resource_path('svg/heroicons/outline'), 'prefix' => 'heroicon-o'],
'brand' => ['path' => resource_path('svg/brand'), 'prefix' => 'brand', 'class' => 'h-5 w-5'],
],
<x-heroicon-o-bell class="h-5 w-5" />
<x-brand-logo class="h-8" />
Subdirectories inside a set path are flattened into hyphenated names: resources/svg/actions/trash.svg → icon-actions-trash. Use that deliberately (folders as namespacing) rather than as an accident.
Raw <x-icon-* /> calls scattered through views are hard to change globally. Add one thin wrapper for the common sizes/states and use it in feature views:
{{-- resources/views/components/ui/icon.blade.php --}}
@props(['name', 'size' => 'w-5 h-5'])
<x-dynamic-component :component="'icon-'.$name" {{ $attributes->class($size) }} />
<x-ui.icon name="camera" class="text-gray-500" />
Pair this with a PHP enum or constants class for icon names so typos fail loudly in tests rather than silently rendering nothing in production.
The directive accepts expressions, which is what you want for data-driven UIs (menu items, plan badges, status indicators):
@foreach ($menu as $item)
<a href="{{ $item['url'] }}">
@svg($item['icon'], 'w-5 h-5 mr-2')
{{ $item['label'] }}
</a>
@endforeach
When the icon source isn't a filesystem directory — a tenant-uploaded logo, an icon chosen from a DB column, a computed path — use the facade instead of relying on config:
use BladeUi\Icons\Factory;
// single closure-backed icon
BladeIcons::registerIcon('tenant-logo', fn () => $tenant->logo_svg);
// or an entire set from a non-standard location
BladeIcons::registerIcons('uploads', fn () => ...);
This is the main extension point and the one to reach for when a set doesn't fit the "directory of files" model. If you need PHP-side rendering (emails, PDFs, JSON APIs), the facade's svg() method returns an HTML string rather than relying on the Blade directive.
Every uncached icon lookup walks the filesystem. Cache the manifest in your deploy pipeline:
php artisan icons:cache
...and remember the inverse — php artisan icons:clear — is what you run whenever the on-disk icon set changes without a fresh build. A typical deploy order is:
php artisan icons:clear
composer install --no-dev --optimize-autoloader
php artisan icons:cache
php artisan config:cache && php artisan view:cache
currentColor is the integration seam with Tailwind. Ship icons with fill="currentColor" / stroke="currentColor" and control colour purely via classes on the component.<x-icon-camera aria-hidden="true" />. Meaningful icons: add role="img" plus <title> in the SVG file, or render visually-hidden text next to it.wire:click, x-on:click, @click pass straight through to the root <svg>.Add one smoke test that iterates your enum/constants list and asserts each icon resolves, so a renamed or deleted file fails CI instead of 500-ing on a page:
foreach (IconName::cases() as $icon) {
$this->blade("<x-icon-{$icon->value} />")->assertSee('<svg', false);
}
The default set is commented out after publishing. The single most common "package is broken" report: @svg('camera') throws SvgNotFound because the set was never uncommented or the path doesn't exist. Validate the path first.
SvgNotFound is an exception, not a blank render. A missing icon in a Blade view becomes a 500 in production. Guard dynamic icon names (@svg($icon ?? 'placeholder'), or a file_exists-style helper at the boundary where the name enters the app) rather than trusting DB/user input.
Stale manifest after deploy is the #1 production mystery. With icons:cache in place, newly deployed SVG files are invisible until the cache is cleared. Make icons:clear idempotent and always run it before icons:cache in the deploy script.
Config changes need a config cache refresh. Editing blade-icons.php in an environment with config:cache has no effect until php artisan config:cache is re-run. Same class of bug, different cache.
Prefix collisions resolve by registration order and will bite silently. Two sets claiming the same prefix, or a set with an empty prefix (only usable via @svg()), produce confusing "wrong icon renders" behaviour. Keep prefixes unique and always non-empty if you want the <x-... /> sugar.
Names are derived from filenames. Camera.svg vs camera.svg, spaces, and odd characters all matter; kebab-case everything and avoid renaming shipped icons — the rename is a breaking change for every template that references them.
Set-level class is a default, not a merge guarantee. Per-set class and attributes act as baseline attributes on the root <svg>; anything you pass on the component/directive is layered on top. If an icon file hardcodes class="w-6 h-6" internally, expect a fight with your Tailwind overrides — strip presentation attributes from the source SVGs and let the calling template own them.
Everything is inlined, so there is no tree-shaking. icons:cache speeds up resolution, not output size. Rendering a 200-row table of icons ships 200 copies of the SVG markup. For icon-heavy UIs, prefer a sprite/<use> approach or a smaller set of glyphs, and keep an eye on response size.
Never render untrusted SVG through this package. SVGs can carry <script> and event handlers. Tenant-uploaded or admin-pasted icons must be sanitised (e.g. with an SVG sanitiser library) before being written to disk or returned from registerIcon, otherwise you have stored XSS with extra steps.
Debugging tip: when an icon "should" work, check in this order — config cache, icons:cache manifest, the actual filename/directory mapping, then the prefix. php artisan icons:clear plus a local @svg() call with a fully-prefixed name isolates the problem to config vs. file layout almost immediately.
Extension points worth knowing: BladeIcons::registerIcon() / registerIcons() (dynamic sources), per-set class/attributes in config (global default styling), the @svg directive (programmatic rendering), and the icons:cache/icons:clear commands (performance and deploy hooks). Almost everything else in day-to-day use is just naming conventions.
How can I help you explore Laravel packages today?