raise-studio / filament-icon-picker
Self-developed, dependency-free Filament form field for picking Heroicons. Zero per-icon server roundtrips, modal grid, recents & favorites, keyboard-first, WCAG 2.1 AA.
Package info
github.com/raise-studio/filament-icon-picker
pkg:composer/raise-studio/filament-icon-picker
Requires
- php: ^8.2
- filament/filament: ^4.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- orchestra/testbench: ^9.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
This package is auto-updated.
Last update: 2026-07-31 04:10:41 UTC
README
π δΈζζζ‘£οΌ readme.zh-CN.md
A self-built, zero extra front-end dependency Filament form icon-picker field component. One-shot SVG map delivery Β· modal grid Β· recents / favorites Β· keyboard-first Β· multi-icon-set extensibility Β· accessibility-friendly.
Provides a handy icon picker inside any Filament form: a styled input triggers a standalone modal with a large icon grid, live search, recents and favorites sections, style switching, and keyboard navigation, plus support for plugging in any third-party or custom icon set.
1. Overview
filament-icon-picker is a Filament form field component package that does exactly one thing well: provide icon-selection capability inside a form.
- Zero extra front-end dependencies: Interaction logic is built on Alpine (bundled with Livewire); assets are registered through
FilamentAssetβ no npm package required. - Performance-first: When the modal opens, the entire
name β inline-SVGmap is fetched in a single request; all subsequent rendering, search, and style switching happen client-side with no per-icon server round-trips. - Multi-icon-set architecture: Ships with Heroicons (Outline / Solid styles) and natively supports plugging in any third-party / custom icon set side-by-side (e.g. Feather, Lucide, or your own SVGs).
- Works out of the box: Auto-discovery of directory-based icon sets,
localStorage-backed recents / favorites, light & dark theme adaptation.
2. Features
- Modal grid picker: A popup independent of the input, a spacious responsive
N-column grid β no more cramped dropdowns. - Live search: Filter as you type, with exact-prefix matches ranked first (150ms debounce by default).
- Recents + favorites: Persisted to
localStorage, pinned at the top, retained across sessions. - Style switching: When the current set supports multiple styles, switch (e.g. Outline / Solid) and preview live inside the modal.
- Keyboard-first:
β β β βmove focus through the grid,Enterselects,Esccloses, typing jumps to the match. - Accessibility: Focus trap + ARIA
dialog/tablist/gridsemantics, WCAG 2.1 AA compliant. - One-click copy: Copy the full class name of the current icon to the clipboard.
- Multi-icon-set: The icon-set switcher bar appears on demand, supporting Heroicons alongside any custom set.
- Zero-cost extension: Drop a folder to plug in, register in one line of code, or implement a contract to take over dynamic / font / remote icons.
- Safe inlining: Third-party SVGs are sanitized by the built-in
SvgSanitizerbefore inlining β strips scripts and event-attribute risks. - Visual polish: Glassmorphic modal, magnetic hover, smooth transitions, automatically following the panel's light / dark theme.
3. Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.2 |
| Laravel | ^12 (required by Filament 4) |
| Filament | ^4 |
spatie/laravel-package-tools |
^1.16 |
- Livewire and Alpine ship with Filament 4 β no separate installation required.
- Built-in icon SVG resources are published with the package β no host-side icon component package needed.
- Zero additional npm dependencies.
4. Installation
4.1 Install via Composer
composer require raise-studio/filament-icon-picker
The service provider is already declared in composer.json's extra.laravel.providers. After Composer installs it, it auto-registers β no manual plugin or service-provider wiring needed.
4.2 Publish front-end assets
JS / CSS are registered via FilamentAsset and must be published once into public/ to be loadable by the browser:
php artisan filament:assets
During development, if you use a path repository + symlink, you still need to re-run the command above after asset changes and hard-refresh the browser (
Ctrl + F5).
4.3 Development (path repository, optional)
Add a path repository with symlink to the host composer.json:
{
"require": {
"raise-studio/filament-icon-picker": "^0.1.0",
"php": "^8.2"
},
"repositories": [
{
"type": "path",
"url": "../packages/icon-picker",
"options": { "symlink": true }
}
]
}
5. Basic Usage
Use the field class inside any Filament Schema / Form:
use RaiseStudio\IconPicker\Forms\Components\HeroiconPicker; HeroiconPicker::make('icon') ->label('Icon') ->allowStyleToggle() // allow Outline / Solid switching (on by default) ->showRecents() // show recents (on by default) ->showFavorites() // show favorites (on by default) ->placeholder('Select an icon');
The field extends Filament\Forms\Components\Field, so all common methods (label / placeholder / required / visible / helperText β¦) are available.
Storage convention: The field always saves a fully-qualified string. The built-in Heroicons set is saved as heroicon-o-xxx / heroicon-s-xxx; a custom icon set is saved in <set>:<style>:<name> format (see Chapter 7). Style switching only changes the o / s prefix or the within-set style β it never loses the full class name, making it easy for the rendering side to fetch the SVG by full name.
6. API Reference & Property List
6.1 Field configuration methods
HeroiconPicker provides the following methods on top of Field:
| Method | Default | Description |
|---|---|---|
->style(string $style) |
'outline' |
Initial style: outline | solid (applies to built-in Heroicons only) |
->allowStyleToggle(bool $v = true) |
true |
Whether to show the style switcher in the modal (only effective when the current set supports multiple styles) |
->showRecents(bool $v = true) |
true |
Whether to show the "Recents" section |
->showFavorites(bool $v = true) |
true |
Whether to show the "Favorites" section |
->recentLimit(int $n) |
12 |
Number of recents retained (minimum 1) |
->gridColumns(int $n) |
8 |
Modal grid columns (responsive, minimum 2) |
->searchDebounce(int $ms) |
150 |
Search debounce in ms (minimum 0) |
6.2 Field state convention
- The database column should be
VARCHARholding the full class-name string β no schema change, zero migrations. - Built-in set:
heroicon-<style>-<name>, e.g.heroicon-o-document,heroicon-s-folder. - Custom set:
<set>:<style>:<name>, e.g.feather:default:home,custom:default:star.
6.3 Consuming the selected icon on the render side
The component only produces / back-fills the class name; rendering is left to the consumer. Fetch the inline SVG by full name:
use RaiseStudio\IconPicker\Icons\IconManifestContract; $svg = app(IconManifestContract::class)->svg($record->icon); // $record->icon can be heroicon-o-document (built-in) or feather:default:home (custom set)
IconManifest::svg() automatically routes the full name to the correct icon set and returns the inline SVG; it returns null when not found.
7. Customizing Third-Party Icon Sets (key section)
The component natively supports plugging in any third-party / custom icon set alongside the built-in Heroicons. The "Icon Set" switcher bar at the top of the modal appears automatically as sets are registered; storage uniformly uses the set:style:name format, fully compatible with the built-in Heroicons data.
7.1 Core concepts & storage format
- Set: An icon library identified by a unique
key(e.g.heroicons/feather/custom), used as the routing and storage prefix. - Style: A variant dimension within a set. Heroicons uses
outline/solid; a single-style set typically usesdefault. - Fully-qualified name:
<set>:<style>:<name>, e.g.feather:default:home,brand:default:logo-a. - Built-in Heroicons keep the legacy
heroicon-o-xxxformat for backward compatibility; both can coexist without interference.
7.2 Method 1: Directory auto-discovery (zero code, recommended for pure SVG libraries)
IconManifest automatically scans the top-level subdirectories of the host application's resources/icons/ directory at construction time: each subdirectory is auto-registered as a DirectoryIconSet, and the modal switcher bar immediately shows that set β drop an SVG folder into your own project and it's plugged in, no PHP required.
β οΈ Only put extension icon libraries into your own application directory β never into the composer-installed
vendor/.../filament-icon-picker/resources/icons/. That directory belongs to the package itself and gets overwritten bycomposer update, so any changes there are guaranteed to be lost.
The default scan path is the application resource_path('icons') (i.e. <your-app>/resources/icons/). It can be overridden via the icon-picker.discover_path config (absolute paths supported). This config ships in config/icon-picker.php; publish it with php artisan vendor:publish --tag=filament-icon-picker-config and edit as needed.
Directory convention (the directory structure is the configuration):
<your-app>/resources/icons/
βββ feather/ β auto-discovered: flat *.svg β single style "default"
β βββ home.svg user.svg β¦
βββ bootstrap/ β multi-style: subfolder name is the style
β βββ outline/*.svg
β βββ solid/*.svg
βββ <your-library>/ β drop it in and it's plugged in
βββ *.svg (or split into styles via outline/ solid/ subfolders)
The built-in Heroicons SVGs live in the package's own
resources/icons/root (read by a dedicated class, not part of auto-discovery). This is package-owned resource β please do not modify or depend on its path.
Rules:
- A directory containing
*.svgdirectly (flat) β single style, style name isdefault; - A directory that also contains subfolders (each holding
*.svg) β subfolder name is the style (e.g.outline/solid); - Optional
meta.jsonto override display name / style list / default style:{ "label": "Lucide", "styles": ["outline"], "defaultStyle": "outline" } - The directory name is auto-converted to a human-readable label (
lucideβLucide,brand-iconsβBrand Icons).
Replace / override an existing set: Calling register() with the same key overrides the original set (see 7.3). For example, to replace the built-in Heroicons entirely, register your own DirectoryIconSet with key heroicons; do not override by dropping same-named files into the vendor directory.
7.3 Method 2: One-line registration (host-directory SVG library)
When the icon library lives in a host project directory (rather than inside the package), use DirectoryIconSet to register it in one line inside the host AppServiceProvider::boot():
use RaiseStudio\IconPicker\Icons\DirectoryIconSet; use RaiseStudio\IconPicker\Icons\IconManifestContract; public function boot(): void { $manifest = app(IconManifestContract::class); $manifest->register(new DirectoryIconSet( 'custom', // key: storage prefix, e.g. custom:default:xxx 'Custom', // label: display name on the switcher bar base_path('resources/icons/custom'), // SVG directory (flat *.svg is fine) ['default'], // styles: style list; pass ['outline','solid'] for multi-style with subfolders 'default', // defaultStyle: default style (optional, defaults to first of styles) true // sanitize: whether to sanitize SVG (default true) )); }
The directory convention is the same as Method 1 (flat = single style default, subfolder name = style), with optional meta.json overriding label / styles.
DirectoryIconSet constructor parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
$key |
string | yes | β | Unique identifier; used as storage prefix and routing. Do not duplicate the built-in heroicons or an already-registered set (duplicate = override) |
$label |
string | yes | β | Display name on the switcher bar; can also be overridden by meta.json |
$basePath |
string | yes | β | Absolute / base path of the SVG directory; flat or subfolder-split styles both work |
$styles |
?array | no | null |
Style list; when null, auto-detected (subfolders β subfolder names, otherwise ['default']) |
$defaultStyle |
?string | no | null |
Default style; when null, takes the first of $styles |
$sanitize |
bool | no | true |
Whether to sanitize third-party SVGs via SvgSanitizer (built-in Heroicons are not sanitized, trusted) |
7.4 Method 3: Implement the contract (dynamically generated / font classes / remote fetch)
When icons are not "a bunch of .svg files in a directory" (inlined-in-code, programmatically generated, remotely fetched, or font-class icons needing custom rendering), implement IconSetContract and register it in the host boot():
| Method | Returns | Description |
|---|---|---|
key(): string |
string | Unique identifier (storage prefix, routing) |
label(): string |
string | Display name on the switcher bar |
styles(): array |
array | Style list; single-style returns ['default'] |
defaultStyle(): ?string |
?string | Default style; returns null when there is no style dimension |
names(string $style): array |
array | Bare icon-name list for a given style (without prefix) |
svgMap(string $style): array |
array | bare-name => inline-SVG, delivered all at once via the icon map |
svg(string $fullOrName, ?string $style = null): ?string |
?string | Resolve and return the inline SVG; return null when not found. Accepts both the new set:style:name format and bare-name + style |
use RaiseStudio\IconPicker\Icons\IconSetContract; class BrandIconSet implements IconSetContract { public function key(): string { return 'brand'; } public function label(): string { return 'Brand'; } public function styles(): array { return ['default']; } public function defaultStyle(): ?string { return 'default'; } public function names(string $style): array { return ['logo-a', 'logo-b']; } public function svgMap(string $style): array { return [ 'logo-a' => '<svg ...>...</svg>', 'logo-b' => '<svg ...>...</svg>', ]; } public function svg(string $fullOrName, ?string $style = null): ?string { if ($style === null && preg_match('/^brand:default:(.+)$/', $fullOrName, $m)) { $name = $m[1]; } else { $name = $fullOrName; } return $this->icons[$name] ?? null; } }
Register (also in host boot()): $manifest->register(new \App\Icons\BrandIconSet());
Registration note:
IconManifestis bound as asingleton. The custom set youregister()in hostboot()resolves to the same instance that the field view data and the icon-map controller use, so nothing is lost.
7.5 Safe sanitization & cache refresh
- Safe sanitization: Third-party SVGs are inlined via
x-html, and by default pass throughSvgSanitizer::clean(), stripping<script>,<foreignObject>,on*event attributes, and dangerous URIs likejavascript:/data:text/html. Built-in Heroicons go through a dedicated trusted class and are not sanitized. For untrusted / complex SVGs, consider a more complete HTML sanitization solution. - Caching: Each set's
names()/svgMap()are cached viaCache::rememberForever.- Add an entire new library directory (Method 1) β auto-discovered, no cache clearing needed;
- Append / modify SVGs in an existing directory (Method 1 / 2) β
php artisan cache:clearto refresh; - Method 3, changed code β
php artisan optimize:clear(or restart the service) to pick up the new class; - After changing package JS / CSS, you must re-run
php artisan filament:assetsand hard-refresh the browser.
7.6 Refreshing built-in Heroicons assets
The built-in Heroicons SVGs ship with the package. To refresh / regenerate the built-in assets:
php artisan filament-icon-picker:sync
8. Assets, Routes & Visuals
8.1 Route endpoints
The component registers two GET endpoints (both called by the front-end on demand):
| Endpoint | Purpose |
|---|---|
/filament-icon-picker/icon-map?set=&style= |
Delivers a set's name => inline-SVG map for a given style in one request (single request, then purely client-side) |
/filament-icon-picker/sets |
Returns the available icon-set list (switcher data source; currently also injected directly via field view data to avoid async failure) |
8.2 Front-end architecture notes
- One-shot SVG map: On modal open,
fetchthe icon map and store it in Alpine state; all subsequent rendering / search / style switching happens client-side with zero server round-trips. - Glassmorphic modal:
backdrop-filterblur + semi-transparent border, 20px rounded corners. - Magnetic hover:
transition: transform .3s cubic-bezier(.16,1,.3,1)+:hover { transform: scale(1.05) translateY(-2px) }. - Light / dark adaptation: Colors reference Filament panel CSS variables, automatically following light / dark / system themes with smooth transitions.
- Scoped: Component styles are uniformly namespaced under the
.fi-icon-picker-*prefix to avoid polluting the global scope.
9. FAQ
Q: Is it compatible with existing data?
A: Fully compatible. The built-in set reads/writes the heroicon-o-xxx full name; custom sets use the set:style:name format. The two can coexist with zero migration.
Q: Can it support icon sets beyond the built-in Heroicons?
A: Natively and at zero cost. A pure SVG library is auto-registered by dropping it into the host application's resources/icons/<library>/ (scans the app's resource_path('icons') by default, configurable via icon-picker.discover_path); a host-directory SVG registers in one line via DirectoryIconSet; dynamic / font / remote icons implement IconSetContract then register(). Storage is unified as set:style:name, and third-party SVGs are sanitized before inlining by default.
Q: Why a modal instead of a dropdown + icon preview? A: With 300+ icons, a dropdown is cramped and the search experience is poor; an independent modal grid is significantly better for preview and retrieval, with zero extra dependencies.
Q: The front-end didn't update after I changed a custom icon set?
A: Appending / modifying SVGs in an existing directory needs php artisan cache:clear; changing JS / CSS needs php artisan filament:assets plus a browser hard-refresh.
Document status: multi-icon-set + modal + set switcher bar + style switching + recents / favorites + keyboard navigation + ARIA + directory auto-discovery + contract extension are all implemented. The three ways to integrate a custom icon set are covered in Chapter 7.