codeat3/blade-vaadin-icons
Blade Vaadin Icons offers a lightweight Blade component for embedding Vaadin icons in Laravel apps. Install via Composer and render any icon by name with a simple tag, keeping markup clean and consistent across views, dashboards, buttons, and navigation.
Install and verify.
composer require codeat3/blade-vaadin-icons:^1.7
Pinning ^1.7 matters: 1.7.0 is the tag that widens framework support to Laravel 13, and earlier 1.x releases constrain Laravel below that. Before committing, open vendor/codeat3/blade-vaadin-icons/composer.json and read the actual laravel/framework and php constraints — changelog wording ("laravel 13 compatibility") is ambiguous about whether versions were widened or dropped.
There is nothing else to wire up. No service provider entry in config/app.php (package discovery handles it), no migrations, no published config, no .env keys, no frontend build step. If you already depend on blade-ui-kit/blade-icons, this slots into that ecosystem rather than adding a parallel mechanism.
First render. Every glyph is a Blade component with a vaadin-icon- prefix:
<x-vaadin-icon-abacus class="w-5 h-5 text-slate-500" />
<x-vaadin-icon-arrow-back class="w-4 h-4" />
The SVG is inlined into the HTML at render time, so standard CSS and Tailwind utilities apply directly.
Where to look first. The authoritative icon inventory is the shipped SVG directory, not the README:
ls vendor/codeat3/blade-vaadin-icons/resources/svg | sed 's/\.svg$//' | column -c 100
Filename normalisation is mechanical: ab-testing.svg → <x-vaadin-icon-ab-testing />, arrow-back.svg → <x-vaadin-icon-arrow-back />. Lowercase, hyphens preserved. When the docs and the resources/svg directory disagree, the directory wins.
First use case. The highest-value first move is a sidebar/nav in an admin panel or internal tool: swap any hand-pasted <svg> markup for <x-vaadin-icon-* /> components one view at a time. It's revertable per-file, so you can trial the aesthetic on a single screen before committing the design language.
Sizing and colour via utilities, not props. These are currentColor-driven SVG roots that pass arbitrary attributes through to the <svg> element. There is no size prop. Control everything with classes:
{{-- inline with text --}}
<x-vaadin-icon-check class="inline-block w-4 h-4 -mt-0.5 text-green-600" />
{{-- large, decorative --}}
<x-vaadin-icon-chart-timeline class="w-8 h-8 text-indigo-500" aria-hidden="true" />
Set explicit w-* h-* every time. Without a size class the SVG falls back to its intrinsic dimensions and you get inconsistent glyph sizes across a page.
Semantic wrapper component — the pattern that pays off daily. Raw icon names leak design decisions into templates. Introduce one indirection so product intent lives in a single map:
// config/icons.php
return [
'dashboard' => 'vaadin-icon-dashboard',
'settings' => 'vaadin-icon-cog',
'delete' => 'vaadin-icon-trash',
];
{{-- resources/views/components/app-icon.blade.php --}}
@props(['name', 'label' => null])
<x-dynamic-component
:component="config('icons.' . $name, 'vaadin-icon-vaadin')"
{{ $attributes->class(['w-5 h-5 shrink-0'])->merge(['aria-hidden' => $label ? null : 'true']) }}
@if($label) role="img" aria-label="{{ $label }}" @endif
/>
Usage becomes <x-app-icon name="delete" class="text-red-500" />. Swapping icon sets later is a config edit, not a find-and-replace across templates. This wrapper is also the only sane place to handle accessibility (see Gotchas).
Global defaults via the blade-icons config. If config/blade-icons.php exists (publish it with php artisan vendor:publish --tag=blade-icons if not — confirm the tag in the publish prompt), its class and attributes keys apply to every icon set in the app. Useful for a house default like 'class' => 'w-5 h-5'; dangerous if you also use another icon family, since you'll be silently restyling it too. Prefer the wrapper component's ->class([...]) for scoped defaults.
Livewire and Inertia-Blasé surfaces. Components work normally inside Livewire views; remember the compiled Blade component is server-side, so re-renders re-emit the SVG. Reuse a wrapper component rather than repeating icon markup in wire:click handlers — a Livewire re-render of a 200-row table with a delete icon per row re-emits 200 SVGs.
Migration from inline SVG or Font Awesome is mechanical. Find-and-replace <i class="fa fa-trash"> / pasted <svg> blocks with <x-app-icon name="..."/>. No data implications, no behavioural change, safe to land view-by-view across several PRs. Do not attempt to preserve old CSS sizing hooks (fa-2x etc.) — map them once to Tailwind classes in the wrapper.
Smoke test in CI. This class of dependency fails silently and visually — no exception, no log, no metric. One test earns its keep:
public function test_vaadin_icon_component_renders(): void
{
$this->blade('<x-vaadin-icon-abacus class="w-4 h-4" />')
->assertSee('<svg', false)
->assertSee('w-4 h-4', false);
}
Add a second assertion that a name from your config/icons.php map resolves, so a typo in the map fails in CI rather than rendering a blank square in production.
An unknown name throws, it doesn't render nothing. A typo like <x-vaadin-icon-arrow-backwards /> produces a Blade Unable to locate a class or view for component exception — loud and caught at request time, but easy to miss if the view is behind a conditional. The wrapper component above with a config fallback (and a __DEV__-only log) converts that into a visible placeholder during development.
Component naming is global. The registered namespace is app-wide. If you already have an in-house <x-vaadin-icon-...> or a conflicting package, you get wrong or missing glyphs with no warning. Grep for the prefix before adopting, and keep your own components on a distinct name (<x-app-icon>).
Clear the view cache after config or version changes. If icons stop resolving after editing config/blade-icons.php or bumping the package, run php artisan view:clear (and config:clear) before debugging anything else. Stale compiled components are the most common "it worked yesterday" cause.
Accessibility is not provided. The components emit bare SVG. Decorative icons need aria-hidden="true"; meaningful icons need role="img" plus an aria-label or a visually-hidden text sibling. Because there is no title prop, this must live in your wrapper — otherwise every developer improvises and you get an inconsistent screen-reader experience. Pair icon-only buttons with text:
<button type="submit" class="...">
<x-app-icon name="save" />
<span class="sr-only">Save changes</span>
</button>
Don't pass class expecting it to override. Blade merges attributes, so a globally configured default from blade-icons.php survives alongside your utility classes and can produce contradictory sizing. Inspect the rendered SVG's class attribute once; if you see duplicates, remove the global default.
They're drawn on a small grid. Vaadin's line-based glyphs look correct at 16–24px and get visually heavy above ~32px, where stroke weight reads as chunky. Reserve the icon set for UI chrome (nav, buttons, table actions, form affordances) and keep hero/marketing imagery elsewhere.
Inline means duplicated payload. A 50-row table with an action icon per row emits 50 identical SVG blocks. Fine at typical admin-panel scale; if you're rendering hundreds of icons in one response, hoist to an SVG sprite (<symbol> + <use>) or cache the rendered fragment. Measure before optimising — SSR inlining is usually cheaper than an extra HTTP request.
Don't edit files in vendor/. If you need a tweaked glyph, copy it into resources/views/components/ under your own namespace. composer update will overwrite anything you touch inside the package, silently.
Not for email or PDF. Many email clients strip inline SVG entirely, and DOM-to-PDF renderers handle it inconsistently. Flatten to raster or use table-based fallbacks for those surfaces — the Blade coupling is a poor fit regardless.
Check the glyph licence, not just the package licence. The package is MIT, but the bundled Vaadin Icons artwork carries its own upstream terms (historically a Creative Commons attribution licence). Confirm the current terms in the vendor directory's LICENSE, and if attribution is required, ship it in an about/credits page rather than discovering it in a legal review.
Version bumps are mostly no-ops. Releases here are driven by upstream framework compatibility rather than new glyphs — 1.7.0 contains no API changes, deprecations, or removals. Take them during routine dependency hygiene, but read the composer.json constraint diff each time; the one diff that matters is a dropped supported version, which turns a no-op into a coordinated upgrade.
How can I help you explore Laravel packages today?