Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Icon Picker Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

  1. 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").

  2. 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.

  3. 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(),
        ];
    }
    
  4. 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.

Implementation Patterns

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.

Gotchas and Tips

  • Maintenance risk is the headline, not a footnote. 3 stars, 0 dependents, September 2023. Treat it as vendored code from day one: pin the version, add it to your Nova upgrade checklist, and be ready to fork it into 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.
  • You cannot meaningfully test this in CI. No Nova licence on the runner means zero automated coverage of the picker. Test what you can: that the attribute round-trips through create/update, and that Icon::render() escapes correctly (<script> in, inert string out). Skip UI assertions.
  • Confirm the wire format before writing anything else. This is the number one way the integration goes wrong, because it fails silently: blank icon, no validation error, no exception. Class string vs. slug vs. icon name — decide by reading the component, not by guessing.
  • Always escape on output. The value comes from an admin form. Never {!! $model->icon !!}, never interpolate into an attribute without e(). Route everything through the helper — that's the whole point of the helper.
  • "The field doesn't appear" is almost always assets, not code. After install or a Nova upgrade: 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.
  • Customising the icon set may require rebuilding JS. If the component filters icons in Vue rather than receiving them from PHP, an 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.
  • Don't build a data contract on the picker output. If icons matter beyond cosmetics (public API, mobile app, theming), store your own icon_key slug column and treat the picker's output as presentation only. That keeps you free to delete the package without touching consumers.
  • Ask whether you need it at all. For a handful of icons, 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.
  • Debugging: distinguish the two failure modes. Saves but renders nothing → 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin