codeat3/blade-teeny-icons
codeat3/blade-teeny-icons is a Laravel Blade component package for easily embedding Teeny Icons SVG icons in your views. Use simple Blade tags, customize size and attributes, and keep icon markup clean and consistent.
Install and render. One command, no build step, no published assets:
composer require codeat3/blade-teeny-icons
The service provider is auto-discovered. Icons are then available as Blade components named after the SVG file:
<x-teeny-icon-check />
That is the whole loop. If the tag renders nothing (or leaks through as literal markup), you have a discovery/autoload problem, not a syntax problem — see Gotchas.
Where to look first. Do not guess icon names from the marketing site. The authoritative list is on disk:
ls vendor/codeat3/blade-teeny-icons/resources/svg # or resources/views/components
Verify the layout before writing anything: this set ships outline and solid variants, so confirm whether that distinction is encoded in the component name (<x-teeny-icon-check-outline />) or elsewhere. Getting that wrong at the start costs you a find-and-replace across every view later. Also check composer.json in the package for the illuminate/support constraint — release 1.12.0 is a Laravel 13 compatibility bump, and the changelog does not say whether the range was widened or replaced. If your app is on Laravel 9–11, confirm your major is still in the range before you build on it.
First use case. Put an icon inside a button and check class passthrough, which is the only API this package really has:
<button class="inline-flex items-center gap-2 rounded bg-indigo-600 px-3 py-2 text-white">
<x-teeny-icon-plus class="h-4 w-4" />
New item
</button>
Then do the same tag in a queued mailable. That second test is the real reason to adopt this package — server-rendered surfaces where an npm icon bundle cannot reach — so validate it on day one rather than at the end of rollout.
Optional config. If the family convention holds, there is a publishable config that sets a default class applied to every icon. Publish and inspect it before you start sprinkling class attributes everywhere; a sensible default (h-5 w-5) here removes a lot of repetition:
php artisan vendor:publish --tag=blade-teeny-icons-config
Treat the config default as your sizing contract. Set one default class globally, then use class on the tag only for the exceptions (small inline icons in dense tables, large empty-state icons). If you instead pass a size to every invocation, you have made sizing a per-view decision that nobody will keep consistent.
Dynamic icons need an allowlist. For menus driven by config or DB, use the dynamic component:
<x-dynamic-component :component="'teeny-icon-'.$item->icon" class="h-5 w-5" />
Never interpolate user input or unvalidated DB values into that string. Blade resolves the component name at runtime; a bad value produces an exception at best and a confusing resolution at worst. Validate against a hardcoded array of permitted names, or better, expose an enum/constant map that your seeders and forms also read from.
Centralize icon decisions in a wrapper. Once the same icon appears with three different sizes and two colours across the app, introduce a thin wrapper and stop calling the vendor tag directly:
{{-- resources/views/components/icon.blade.php --}}
@props(['name', 'size' => 'md'])
<x-dynamic-component
:component="'teeny-icon-'.$name"
{{ $attributes->merge(['class' => match($size) {
'sm' => 'h-4 w-4',
'lg' => 'h-6 w-6',
default => 'h-5 w-5',
}]) }}
aria-hidden="true"
focusable="false"
/>
This is also your migration seam: if you later move to an owned design-system icon package, you rewrite one file instead of every view.
Decorating vs. labelling. Icons beside text should be aria-hidden="true" and focusable="false" (the wrapper above handles it). Icon-only buttons need an accessible name on the button, not the SVG:
<button type="button" aria-label="Close">
<x-teeny-icon-x class="h-4 w-4" aria-hidden="true" />
</button>
Because the wrapper forces aria-hidden, don't use it for the meaningful-icon case — call the raw component there or add an escape prop.
Cache icon-dense fragments. Inline SVG is emitted per usage and is not deduplicated: a table rendering the same icon in fifty rows ships fifty SVG bodies. On dashboards, data tables, and settings pages, either cache the rendered fragment or reduce the icon to a single inline <svg><use href="#..."/></svg> sprite pattern. Measure the HTML payload on your densest page before rollout, not after.
Email and PDF are separate rendering paths — test them separately. In mailables, Tailwind classes and <style> blocks are stripped by major clients; a class="h-4 w-4" icon will come through at full size. Use presentational attributes there instead of classes. For PDF generation, inline SVG support is engine-specific: browser-based engines (Browsershot, Snappy) are fine, but dompdf has poor inline SVG support and you may need a data-URI <img> or a PNG fallback. Decide this per engine before promising icons in invoices.
Test the component surface, not the pixels. A cheap feature test that renders a fixed list of icon names catches upstream renames, typos, and config breakage in CI:
foreach (['check', 'plus', 'x'] as $icon) {
$this->blade("<x-teeny-icon-{$icon} />")->assertSee('<svg', false);
}
That test is also your upgrade tripwire when a new package version lands.
Component names change; icon sets rename glyphs. This is the one failure mode with real blast radius: an upstream rename silently breaks tags that the compiler cannot validate. Pin the package version in composer.json (not a floating ^) and let the smoke test above fail before you discover it in production.
Unresolved components fail quietly. If <x-teeny-icon-check /> renders as nothing or as raw HTML, the component isn't registered. Run php artisan view:clear first, then composer dump-autoload, then confirm the file actually exists in the vendor resources directory. A missing icon name is a different error (an exception), so "nothing rendered" almost always means discovery, not naming.
Your class may not win on size. If the component hardcodes width/height attributes, a Tailwind h-4 w-4 class fights those attributes and CSS specificity decides the outcome — sometimes the attribute wins. Check the rendered markup in devtools. If attributes win, either set the size via the config default, or use the ! important modifier on the utility as a deliberate last resort, not a habit.
Pin down the 1.12.0 constraint semantics before upgrading. The changelog says "laravel 13 compatibility" and nothing else. Diff composer.json between 1.11.0 and 1.12.0, then run composer why-not laravel/framework '^13.0' on your branch. If you are on an earlier major, verify your range still resolves after the bump — a rewritten constraint can drop you.
Watch for namespace collisions and boot cost. All codeat3/blade-* packages register their full icon set at boot. The teeny-icon- prefix makes literal collisions unlikely, but if you co-install Heroicons or Tabler, confirm the prefixes are actually distinct and check whether boot time moves on a cold cache. It is a modest cost, but it is silent when it bites.
config:cache freezes your default class. Changing the config default then wondering why icons still render at the old size is a classic. php artisan config:clear after any config edit.
CSP is mostly a non-issue, with one caveat. Inline SVG avoids img-src entirely, which is a genuine advantage over icon fonts and image sprites. But if you adopt a <use href="#sprite"> optimization, the sprite must be in the DOM on every page, and any style="fill:..." attributes you add will need to survive a strict style-src policy. Prefer fill/stroke as presentation attributes over style.
Escape hatch is cheap — plan it. Icons are static SVG and the package has no runtime dependencies and no meaningful external validation (0 stars, 0 dependents, single maintainer, compatibility-only release cadence). If upstream goes quiet or misses a Laravel major, vendoring the SVG directory into your own repo and swapping the component namespace is a few hours of work. Decide who owns that contingency now, because the package itself is not going to tell you when it's time.
How can I help you explore Laravel packages today?