nevadskiy/nova-icon-picker
A Laravel Nova field that adds an icon picker to your admin panel, letting you search and select icons from supported icon sets and store the chosen icon name on your models. Simple to install and configure for Nova resources.
This is a Laravel Nova form field and nothing else. If the app isn't running Nova, stop reading. The entire value proposition is: a searchable, visual icon grid instead of an editor hand-typing fa-solid fa-user.
Check Nova compatibility first, before you spend an hour on the integration. The last release is 2023-09-26, which predates Nova 5. Assume Nova 4.x until you've read the composer.json constraint in the package. On Nova 5, timebox a spike — don't promise the picker to a product owner until you've seen it render.
Install and pin immediately. With 3 stars and 0 dependents, a silent patch bump is not something you want arriving unannounced on a Friday:
composer require nevadskiy/nova-icon-picker
Then edit composer.json to an exact version ("nevadskiy/nova-icon-picker": "1.x.y").
Read the Vue component before you write the migration. The stored value is a plain string, but which string is the single most important unknown. It might be a full CSS class (fa-solid fa-user), a slug (user), or a library-specific icon name. Open vendor/nevadskiy/nova-icon-picker/resources/js/ and look at what the component emits:
Schema::table('categories', function (Blueprint $table) {
$table->string('icon')->nullable()->after('name');
});
Also verify the PHP namespace in src/ — small packages rarely guard this with tests, and the docs may not match the class name.
Register the field on one resource. Start with a category or navigation item:
use Nevadskiy\NovaIconPicker\IconPicker;
public function fields(NovaRequest $request)
{
return [
IconPicker::make('Icon', 'icon')
->rules('nullable', 'string', 'max:255')
->stacked(),
];
}
Ship the smallest full loop first. Create → save → index → render in Blade:
<i class="{{ e($category->icon) }}"></i>
That one loop answers the two questions that matter: what string actually lands in the column, and does the bundled icon set match the CSS library your frontend already loads. Everything else is built on those two answers.
The package is small enough that src/ and resources/js/ are the documentation. Read them in vendor/ — you'll need to know their shape the first time Nova upgrades anyway.
Never render the attribute raw; funnel it through one helper. The value originates in an admin form and flows into Blade. Centralise it so escaping and library-swapping happen in exactly one place:
// app/Support/Icon.php
final class Icon
{
public static function render(?string $value, string $class = 'w-4 h-4'): string
{
return $value ? e($value) : '';
}
}
<i class="{{ \App\Support\Icon::render($model->icon) }}"></i>
If you expect to change icon libraries (Font Awesome → Heroicons → Lucide), store a canonical slug (user, cart) and map slug → class inside this helper. That's the difference between editing one method and running a data migration plus a content re-entry project.
Picker on forms, glyph on index/detail. An icon class string in a table cell is noise:
IconPicker::make('Icon')->onlyOnForms()->hideFromIndex()->hideFromDetail(),
Text::make('Icon', fn () => $this->icon
? '<i class="'.e($this->icon).'"></i>'
: '—')
->asHtml()
->onlyOnIndex(),
One field factory per app. The moment a second resource carries an icon (menus, badges, dashboard tiles), rules and help text start drifting:
protected function iconField(string $name = 'Icon'): IconPicker
{
return IconPicker::make($name)
->rules('nullable', 'string', 'max:255')
->help('Pick a glyph; it renders on the public site.')
->default(null)
->stacked();
}
Write your own server-side validation — the field won't. Even if the Vue component restricts the visible set, the payload is editable. Define the allowed list once (a config array or a PHP enum) and pass the same array to both the picker options and a Rule::in($allowedIcons) in your Form Request.
Seeders, imports and API payloads must use the exact emitted format. After one manual save, snapshot the value and treat it as canonical:
php artisan tinker --execute="dump(DB::table('categories')->value('icon'));"
If the picker writes fa-solid fa-user and your seeder writes user, nothing errors — you just get blank glyphs in production and a confusing afternoon of debugging.
Gate the rollout. You can't CI-test the UI without a Nova licence, so ship it behind a config toggle or a canSee callback. A broken picker then degrades to a hidden field rather than a broken admin panel:
IconPicker::make('Icon')->canSee(fn () => config('features.icon_picker')),
Pair it with adjacent fields. Put the picker in the same panel as Name so editors see the label and glyph together, and use ->stacked() — an icon grid is a wide popover and Nova's default half-width field feels cramped.
packages/nova-icon-picker. A Nova field is roughly one PHP class plus one Vue component — forking is genuinely cheap, which is the main argument for adopting it anyway.Icon::render() escapes correctly (<script> in, inert string out). Skip UI assertions.{!! $model->icon !!}, never interpolate into an attribute without e(). Route everything through the helper — that's the whole point of the helper.php artisan nova:publish, then view:clear and config:clear, then a hard refresh. Verify the compiled field exists under public/vendor/ and watch the browser console for a failing app.js.options()/icons() method either doesn't exist or won't be enough — you'll need a fork plus npm run prod inside the package directory. Confirm this before promising a curated set to a product owner.icon_key slug column and treat the picker's output as presentation only. That keeps you free to delete the package without touching consumers.Select::make('icon')->options([...]) plus a small preview Text field is a ten-line, zero-dependency alternative that survives every Nova upgrade. This package earns its place only when the set is large enough that a flat dropdown becomes unusable.dd($model->getRawOriginal('icon')). If the value looks like a valid class, your frontend isn't loading that icon library (or the class is Font Awesome v4/v5-incompatible). If the value looks wrong, the picker's wire format isn't what you assumed. Different problems, different fixes.How can I help you explore Laravel packages today?