marshmallow/nova-fontawesome
marshmallow/nova-fontawesome adds a searchable Font Awesome icon picker field to Laravel Nova. Let editors choose icons visually, store the selected class on your model, and render icons consistently across your admin resources. Lightweight and easy to install.
This package adds one thing to your Nova toolbelt: a searchable Font Awesome icon picker that writes a class string (fa-solid fa-house) to a plain model column. Everything else — how the glyph renders, whether Pro icons are available, whether the admin bundle ships Font Awesome at all — is your application's responsibility. Treat it as a form control, not an icon system.
1. Install and check what you actually got.
composer require marshmallow/nova-fontawesome
composer show marshmallow/nova-fontawesome
Nova 5 only. v3.1.0 added Laravel 13 support — that's a constraint bump, not a behaviour change, and Nova's own Laravel support is the binding constraint in almost every upgrade conversation. Don't assume this field alone unblocks a framework bump.
2. Read the package before trusting a snippet. Open src/ and composer.json. You want three facts:
publishes() and any Nova::script() call, and note the exact publish tag).3. Add the column. One nullable string per icon attribute:
Schema::table('categories', function (Blueprint $table) {
$table->string('icon')->nullable()->after('name');
});
Add it to $fillable if you mass-assign; Nova will otherwise set the attribute directly.
4. Register the field on a low-risk resource.
use Marshmallow\NovaFontawesome\NovaFontawesome;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Name'),
NovaFontawesome::make('Icon')
->help('Shown in the customer-facing category nav.'),
];
}
First use case: a category badge, nav marker, or status label — something visible enough that you'd notice a broken picker, contained enough that a bad release doesn't block a publish action. Create or edit a record, pick an icon, save, confirm the string landed in the database, then confirm the glyph appears in your frontend template. That loop is your acceptance test for the whole package.
Centralize the field on day two, not day thirty. The moment two resources share an icon column, drift begins — different rules, different help text, one with hideFromIndex() and one without. Put it behind a trait or base resource method:
trait HasIconField
{
protected function iconField(string $attribute = 'icon'): NovaFontawesome
{
return NovaFontawesome::make($attribute)
->rules('nullable', 'string', 'regex:/^fa-[a-z0-9]+( fa-[a-z0-9-]+)+$/')
->help('Font Awesome 6/7 icon class.');
}
}
That is now the single place to change validation, visibility policy, or swap in a fallback text input later.
Decide raw class vs. semantic key at adoption. Storing fa-solid fa-house is the zero-friction path — it's exactly what the picker emits and exactly what your Blade needs. The cost is coupling: an FA 6→7 rename becomes a data migration, and per-tenant theming means rewriting rows. If icons might ever be white-labelled or swapped by non-engineers, store a key and map it:
// config/icons.php
return [
'category-house' => 'fa-solid fa-house',
'category-archive' => 'fa-solid fa-box-archive',
];
@php($icon = config('icons.' . $category->icon, 'fa-solid fa-circle-question'))
<i class="{{ $icon }}" aria-hidden="true"></i>
You lose the picker's direct output and need a translation step at every render site. Only pay that if a non-FA future or per-tenant overrides are genuinely plausible. What you must not do is leave the decision unmade and discover six months of mixed formats in production.
Whitelist at every render site. The field guarantees a string was chosen once. It does not guarantee that string is valid today — FA renames happen, rows get hand-edited, imports are wrong. Never interpolate raw:
@php
$raw = $category->icon;
$safe = is_string($raw) && preg_match('/^fa-[a-z0-9]+( fa-[a-z0-9-]+)+$/', $raw)
? $raw
: 'fa-solid fa-folder';
@endphp
<i class="{{ $safe }}" aria-hidden="true"></i>
{!! $category->icon !!} is an injection vector the moment anyone with Nova access can type into the field — and a nullable column edited in an admin panel is user input.
Make the index view readable. A raw class string in a table cell is noise. Render the glyph with asHtml() and an escaped class:
Text::make('Icon', fn ($category) => $category->icon
? '<i class="'.e($category->icon).'"></i>'
: '—')
->asHtml(),
This turns the index into a quick visual audit — you can spot format drift and missing icons at a glance instead of opening records.
Seed and backfill. Existing rows are NULL, so every list view is blank on rollout day. Give factories a default and backfill the table before you remove any legacy icon column:
Category::whereNull('icon')->update(['icon' => 'fa-solid fa-folder']);
Smoke-test inside repeaters and panels before you commit. Single-purpose Nova fields most often break when mounted somewhere the component didn't anticipate — Repeater, Panel, nested Flexible content. It's a five-minute staging check that saves you discovering it after the resource is live.
Keep a text-input fallback behind a flag. If a future release breaks the picker, editors are stuck. An ->onlyOnDetail() plain Text::make('Icon') variant, or a config-gated swap, costs almost nothing and means an abandoned dependency can't block an admin release.
Nova::style(), or your own compiled CSS. A picker full of invisible boxes reads as broken even when it's functioning perfectly.null. Depending on the component path, Nova may persist ''. Use blank() / filled() in accessors and conditionals rather than === null.fa-solid fa-house in one resource, fas fa-house or bare house in another. The regex rule plus a centralized field definition is the whole defence. Write the convention into your adoption note so the next developer doesn't invent a third format.vendor:publish --tag= silently does nothing and you'll spend an hour wondering why assets aren't there. After any Nova or package upgrade, run php artisan nova:publish and clear cached config/views — stale published Vue assets are the classic "picker renders blank" cause.^3.1, note the resolved version in your lockfile notes, and upgrade on a schedule rather than absorbing an untested minor bump in a field nobody watches.->rules(), ->help(), ->onlyOnForms()/->hideFromIndex(), subclassing the field, and overriding the Vue component if you need custom behaviour. There is no plugin surface. That's fine for a one-field package, but accept up front that a genuine feature request means vendor code, not config.How can I help you explore Laravel packages today?