driftyco/ionicons
Ionicons is an open-source icon set with premium SVG and web font icons for Ionic Framework and web apps. It offers hundreds of crisp, scalable icons in outline, filled, and sharp styles, with easy usage via CSS, SVG, and framework components.
Ionicons is an icon set, not a Laravel package — there is no service provider, no config file, no facade, and no PHP code. Treat it as a frontend dependency and wire it in yourself. Budget ten minutes.
vendor/:npm i ionicons
resources/js/app.js:import { defineCustomElements } from 'ionicons/dist/loader';
defineCustomElements(window);
<ion-icon name="heart-outline"></ion-icon>
That is the whole first use case: a decorative icon in a nav bar or button.
Where to look first:
node_modules/ionicons/dist/svg/ — every icon as a standalone .svg file. Open a few to see the actual markup before you commit to the web-component approach.node_modules/ionicons/dist/ionicons/ — the compiled loaders (ionicons.esm.js, ionicons.js) if you decide to go the CDN route instead of Vite.The decision you are actually making on day one is web component vs. inline SVG. The web component is five lines of setup and handles every icon in the set; inline SVG means writing a ~40-line Blade component but ships zero runtime JavaScript. For a marketing site, inline. For an admin panel behind auth, the web component is fine.
If you want server-rendered icons without writing your own component, blade-ui-kit/blade-icons supports registering a custom icon set pointing at the SVG directory — that gives you <x-icon-heart-outline /> with no JS at all.
Blade component wrapper. The single highest-leverage thing you can build is a wrapper that standardises sizing, accessibility, and the variant suffix:
{{-- resources/views/components/icon.blade.php --}}
@props(['name', 'variant' => 'outline', 'size' => 'md', 'label' => null])
@php
$sizes = ['sm' => 'text-base', 'md' => 'text-xl', 'lg' => 'text-3xl'];
$icon = $variant === 'filled' ? $name : "{$name}-{$variant}";
@endphp
<ion-icon
name="{{ $icon }}"
class="{{ $sizes[$size] ?? $sizes['md'] }} {{ $attributes->get('class') }}"
@if($label) aria-label="{{ $label }}" role="img" @else aria-hidden="true" @endif
></ion-icon>
Call site: <x-icon name="trash" variant="solid" class="text-red-600" />. The variant default prevents the most common bug — forgetting the suffix and rendering nothing.
Size and colour via inherited CSS. Ionicons are text-shape icons: they inherit font-size and color, and stroke weight is tunable per-element. In app.css:
ion-icon {
color: currentColor;
--ionicon-stroke-width: 32px;
}
Because of this, Tailwind's text-xl and text-slate-500 work directly on the element. No w-6 h-6 — those do nothing on a <ion-icon>.
Variant strategy. v7 has three weights per icon: base (filled), -outline, and -sharp. Pick one as your house style and encode it in the wrapper's default. Mixing outline in the sidebar with filled in the toolbar looks accidental, not designed.
Dynamic names in Livewire/Alpine. The name attribute is reactive, so bindings work:
<ion-icon :name="$store.ui.expanded ? 'chevron-down' : 'chevron-forward'"></ion-icon>
For Livewire, plain name="{{ $expanded ? 'chevron-down' : 'chevron-forward' }}" is fine — Livewire morphs the attribute and the web component re-renders. Do not try to pass an icon name through wire: directives; compute it in the component property or Blade expression instead.
Sprite/inline path for server rendering. If you need icons inside PDFs (DomPDF), emails, or an OG-image generator, the web component is useless — there is no JS. Copy node_modules/ionicons/dist/svg/*.svg into resources/svg/ionicons/ via a Vite plugin or a postinstall script, then:
public function icon(string $name): string
{
return Cache::rememberForever("icon:{$name}", fn () =>
File::get(resource_path("svg/ionicons/{$name}.svg"))
);
}
Echo it inside a wrapper with fill="currentColor" stripped of fixed width/height.
CSP-strict environments. Do not use the unpkg CDN loader — it injects a script tag and will trip script-src. Bundle via Vite so the loader is a same-origin module.
Icons render blank, not broken. If name is wrong, <ion-icon> renders an empty box with no error, no console warning, no 404. This is the top debug sink. When an icon is missing, verify the exact filename in dist/svg/ — the variant suffix is part of the name (trash-outline, not trash).
v5 → v7 renamed everything. The old ios-/md- prefixes are gone; what was md-heart is now heart, and ios-heart-outline is now heart-outline. Any tutorial or Figma file older than 2022 will hand you names that silently render nothing. Do a repo-wide regex for \b(ios|md)- on upgrade.
FOUC before hydration. Between HTML parse and the loader executing, <ion-icon> is an unknown element and occupies no space; icons pop in and shift layout. Mitigate with a .ionicon-pending class removed on defineCustomElements().then(...), or reserve space with min-width/min-height on the element. On a slow connection this is very visible on a settings page with 30 icons.
Bundle weight. The loader is roughly 80–120 KB gzipped — a lot for six icons. If your usage is small and static, inline SVGs and skip the dependency. If you are already shipping Ionic Framework, you pay this cost anyway.
Accessibility is opt-in. A bare <ion-icon name="close"> is invisible to screen readers and ambiguous to sighted users. Decorative icons need aria-hidden="true"; meaningful ones need aria-label and role="img". Bake both branches into the Blade wrapper so developers cannot forget.
--ionicon-stroke-width is per-element and inherited. Setting it globally is fine, but if you set it on :root it may be overridden by Ionic's own theme variables if Ionic Framework is also installed — check the computed value rather than the stylesheet.
No config, no extension points, no versioning contract for you. Because this is a pure asset dependency, there is nothing to "configure." Your extension points are entirely yours: the Blade component, a Tailwind plugin entry for icon sizes, and the SVG cache helper. Keep those three in one place — app/View/Components/Icon.php plus resources/svg/ — so an upgrade is a npm update and a filename diff rather than a code hunt.
Debugging checklist, in order: does the loader run before the icon element? (Network tab: is the .esm.js chunk 200?) → is the name in dist/svg/? → is the element upgraded? ($0.constructor.name in the console should not be HTMLElement) → is CSS visibility or a display: none parent hiding it? That sequence resolves almost every report.
How can I help you explore Laravel packages today?