twbs/bootstrap-icons
Official open source SVG icon library for Bootstrap, offering over 2,000 free, MIT-licensed icons. Install via npm (bootstrap-icons) or Composer, then use as embedded HTML, img tags, an SVG sprite, or CSS. Also available in Figma.
Two install paths, and you should pick one deliberately:
# Front-end (recommended when you already run Vite)
npm i bootstrap-icons
# Back-end only (no Node in the build, or you just need the raw SVGs)
composer require twbs/bootstrap-icons
Neither package ships a Laravel service provider, so there is no vendor:publish. After installing, immediately confirm what you actually got:
ls vendor/twbs/bootstrap-icons # icons/ font/ bootstrap-icons.svg package.json
ls node_modules/bootstrap-icons # same layout
The three artefacts that matter day-to-day:
| Artefact | Path | Use |
|---|---|---|
| Individual SVGs (~2,000 files) | icons/*.svg |
Inline Blade component, per-icon control, currentColor |
| Icon font + CSS | font/bootstrap-icons.css, font/bootstrap-icons.min.css, font/fonts/*.woff2 |
Fastest to author, <i class="bi bi-alarm"></i> |
| Sprite | bootstrap-icons.svg |
One HTTP request for many icons |
First use case — a sidebar nav in your existing layout. Wire the font once in resources/sass/app.scss:
@import "bootstrap-icons/font/bootstrap-icons.css";
include it via @vite(['resources/sass/app.scss']), then in resources/views/layouts/app.blade.php:
<nav class="nav flex-column gap-1">
@foreach (config('nav.items') as $item)
<a class="nav-link d-flex align-items-center gap-2"
href="{{ route($item['route']) }}">
<i class="bi bi-{{ $item['icon'] }}" aria-hidden="true"></i>
{{ $item['label'] }}
</a>
@endforeach
</nav>
That is the five-minute version. Everything below is about making it survive production.
The font is convenient but loads ~100 KB of woff2 and is invisible until it arrives (font-display: block was added in v1.8.2). For anything above the fold, inline the SVG so it paints immediately and inherits currentColor:
// app/View/Components/Icon.php
namespace App\View\Components;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\Component;
class Icon extends Component
{
public string $name;
public function __construct(string $name)
{
// Never let a DB/user-supplied value reach the filesystem raw.
$this->name = preg_replace('/[^a-z0-9\-]/', '', strtolower($name));
}
public function inner(): string
{
$file = base_path("vendor/twbs/bootstrap-icons/icons/{$this->name}.svg");
if (! $this->name || ! is_file($file)) {
return '';
}
return Cache::rememberForever(
'bi:'.$this->name.':'.filemtime($file),
fn () => preg_replace(
'/^.*?<svg[^>]*>|<\/svg>.*$/s', '', file_get_contents($file)
)
);
}
public function render()
{
return view('components.icon');
}
}
{{-- resources/views/components/icon.blade.php --}}
@if ($inner = $this->inner())
<svg {{ $attributes->merge([
'width' => '1em', 'height' => '1em',
'fill' => 'currentColor', 'aria-hidden' => 'true',
'focusable' => 'false',
]) }}>{!! $inner !!}</svg>
@endif
Usage is then uniform everywhere: <x-icon name="trash" class="text-danger" />. Because you strip the outer <svg> and re-emit your own attributes, width="1em" scales with font-size, and class="fs-4" on a parent works as expected.
blade-ui-kit/blade-icons own the lookupIf you already use @svg() in views, register Bootstrap Icons as a set instead of writing the component:
// config/blade-icons.php
'sets' => [
'bi' => [
'path' => 'vendor/twbs/bootstrap-icons/icons',
'prefix' => 'bi',
],
],
Then @svg('bi-alarm', 'w-4 h-4 text-secondary') in any Blade file, plus <x-bi-alarm />.
Admin tables with an action icon per row generate hundreds of inlined SVGs. Ship one sprite instead:
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [laravel({ input: ['resources/js/app.js'], refresh: true })],
publicDir: 'public',
});
Copy node_modules/bootstrap-icons/bootstrap-icons.svg into public/build/ (or a public/vendor/icons directory committed/vendored at build time), then:
<svg class="bi" width="16" height="16" aria-hidden="true">
<use href="{{ asset('vendor/icons/bootstrap-icons.svg') }}#pencil-square"></use>
</svg>
Same-origin only, and <use> on external files does not inherit currentColor in every browser — verify in Safari before committing to it.
font/bootstrap-icons.json is a name → codepoint map. Use it to keep icon names out of free-text DB columns:
// app/Rules/IconName.php
public function passes($attribute, $value): bool
{
return array_key_exists(
$value,
json_decode(file_get_contents(base_path('vendor/twbs/bootstrap-icons/font/bootstrap-icons.json')), true)
);
}
The same JSON powers a Livewire/select2 icon picker in an admin panel: build the options once, cache them, and you get a searchable visual picker for ~2,000 icons for free.
If your build pipeline is Mix or you deploy --no-dev, vendor/ is wiped on every deploy and is not web-served. Add a tiny copy step rather than referencing vendor/ from asset():
// app/Console/Commands/PublishIcons.php
$src = base_path('vendor/twbs/bootstrap-icons/font');
File::ensureDirectoryExists(public_path('vendor/bootstrap-icons'));
File::copyDirectory($src, public_path('vendor/bootstrap-icons'));
Run it in the deploy script after composer install and before php artisan optimize. Then <link rel="stylesheet" href="{{ asset('vendor/bootstrap-icons/bootstrap-icons.min.css') }}">. For Mix, mix.copyDirectory('node_modules/bootstrap-icons/font/fonts', 'public/vendor/bootstrap-icons/fonts') alongside the Sass import is the equivalent.
cassette/cassette-fill; v1.10.0 renamed building → buildings. A composer update (or a floating ^1.10 in package.json) can silently blank out an icon in a template nobody retested. Pin the minor ("1.10.*"), commit the lockfile, and add a smoke test that renders every icon name in config/nav.php and asserts a non-empty <svg>.vendor:publish --tag=icons; do not copy tutorials that assume one. Everything you get, you move yourself.font/fonts/. bootstrap-icons.css references ./fonts/bootstrap-icons.woff2 relatively. Copying only the CSS gives you tofu boxes (▯) and a 404 in the network tab. This is the single most common misconfiguration.class="bi" on an inline SVG does nothing helpful. The bi class comes from the font stylesheet. When inlining, drop it and control size with width/height or CSS — otherwise you get display: inline-block and baseline gaps.{!! !!} icon name. The inner-SVG pattern above needs unescaped output by design; sanitise the name in the component constructor (as shown) and whitelist it in any FormRequest that accepts one.aria-hidden="true" and focusable="false" (older IE/Edge make SVGs tabbable otherwise). Icon-only buttons need their own label: <button aria-label="Delete"><x-icon name="trash" /></button>, or add <title> inside the SVG when the icon carries meaning on its own.<svg>; dompdf's SVG support is partial at best. For notifications and invoices, either use a hosted PNG or accept text labels. Don't discover this after building an email template.npm run icons / npm start are maintainer scripts. The repo is a Hugo documentation site. Cloning it and building is for contributing glyphs, not for consuming them — the npm/Composer packages ship pre-processed SVGs already run through SVGO.icons/ directly), file read blocked by permissions in the container, the inner-SVG regex not matching because the file has an XML prolog, or a stale cache entry keyed only on the name. Include filemtime($file) in the cache key, as above, and php artisan view:clear when you swap the component implementation.blade-icons set config (path/prefix/fallback), the icons/*.svg directory itself if you vendor a patched copy, and the CSS custom properties around currentColor on the font class. The package itself is intentionally dumb — "we only include the processed SVGs; it's up to you and your team to implement."How can I help you explore Laravel packages today?