itsjavi/fontawesome-iconpicker
A lightweight jQuery plugin that adds a searchable Font Awesome icon picker to text inputs. Browse, filter, and select icons from a clean popover UI, with support for multiple Font Awesome versions, keyboard navigation, and easy setup in any web form or admin panel.
This is not a Laravel package — it's a jQuery/Bootstrap frontend widget published on npm as fontawesome-iconpicker. Treat it as an asset with a Blade wrapper, and keep all icon logic on the frontend.
1. Install the assets
npm install fontawesome-iconpicker jquery bootstrap
2. Load order matters (jQuery → Bootstrap → picker). In resources/js/app.js:
import $ from 'jquery';
window.$ = window.jQuery = $; // required: the plugin registers itself on the global jQuery
import 'bootstrap';
import 'fontawesome-iconpicker';
import 'fontawesome-iconpicker/dist/css/fontawesome-iconpicker.min.css';
If you're on Vite, also double-check optimizeDeps/build.rollupOptions aren't duplicating jQuery — two jQuery instances is the #1 "plugin is not a function" cause.
3. Minimum viable field
{{-- resources/views/components/icon-field.blade.php --}}
@props(['name', 'value' => null])
<div class="input-group">
<span class="input-group-text"><i id="{{ $name }}-preview" class="{{ $value }}"></i></span>
<input type="text"
name="{{ $name }}"
id="{{ $name }}"
class="form-control iconpicker"
value="{{ $value }}"
data-selected="{{ $value }}"
autocomplete="off">
</div>
$('.iconpicker').iconpicker({
title: 'Choose an icon',
selected: $(this).data('selected') || '',
placement: 'bottom',
search: true,
});
4. First real use case — a menus or categories table with an icon column you render in a sidebar. Add $table->string('icon')->nullable() and output it as <i class="{{ $item->icon }}"></i>. That's the sweet spot for this library: an admin CRUD form where a human picks a class string.
Where to look first: the dist/ folder for the built CSS/JS, and the demo page's option list. Skip the source unless you need to fork it (you may — see Gotchas).
Make it a Blade component, not inline markup. Every CRUD form ends up with the same input group; centralise it as x-icon-field and pass the current value in. Keep a <i> preview next to the input and update it in the iconpickerSelected handler.
Store the raw class string, render defensively. The picker emits a fa fa-* (Font Awesome 4-era) string. Store that string in one column (string('icon')->nullable()) and render via a single <x-icon :class="$icon" /> component so you can normalise prefixes in one place later.
Wire up the events properly. The widget doesn't fire a native change, so anything reactive (Alpine, Livewire, your own preview) needs its own listener:
document.addEventListener('DOMContentLoaded', () => {
$('.iconpicker').each(function () {
const $input = $(this);
$input.iconpicker({ selected: $input.val() });
$input.on('iconpickerSelected', function (e) {
const icon = e.iconpickerValue; // e.g. "fa fa-user"
$input.val(icon).trigger('change'); // keep Livewire/Alpine in sync
$('#' + this.id + '-preview').attr('class', icon);
});
});
});
Triggering a native change on the underlying input is what makes it cooperate with wire:model and x-model.
Livewire: re-init after every morph. Livewire wipes the popover DOM on re-render. Register a hook once and re-run initialization on morph.updated:
document.addEventListener('livewire:init', () => {
Livewire.hook('morph.updated', ({ el }) => {
$(el).find('.iconpicker').each(function () {
if (!$(this).data('iconpicker')) $(this).iconpicker();
});
});
});
Alternatively, keep the icon field in a plain (non-Livewire) form and submit normally — far less friction.
Multiple pickers on one page. Loop with $(this) inside .each() (as above) rather than calling .iconpicker() on the whole collection with shared options — otherwise selected is applied identically to every input.
Restrict the iconset. The picker ships a Font Awesome 4.7 icon list. If you only want a curated set (say, 40 navigation icons), pass your own array rather than forcing admins to scroll past 600:
const navIcons = ['home', 'user', 'cog', 'envelope', 'chart-bar', 'calendar'];
$('.iconpicker').iconpicker({
iconset: [{ title: 'Navigation', icons: navIcons }],
});
This also makes the component feel intentional instead of like a dump of an entire icon library.
Validation on the server. Don't trust the posted class string — it ends up inside a class attribute. Use a FormRequest with a regex rule and a whitelist:
'icon' => ['nullable', 'string', 'max:100', Rule::in(config('icons.allowed'))],
At minimum: regex:/^fa[srb]? fa-[a-z0-9-]+$/. This kills the "><script> class of problem before it reaches Blade.
Seed and edit flows. For an edit form, set selected from the model and also pre-fill the input's value so the field isn't blank before JS boots. Progressive enhancement: the value is correct even if the JS bundle fails to load.
Testing. Add one Dusk/browser test that opens the picker, clicks an icon, and asserts the hidden value changed. Unit tests can't cover this widget, and the "it silently doesn't init" failure mode is invisible to feature tests.
fa fa-name. If your app uses FA5/FA6 (fa-solid fa-user), the picker's class strings won't render. Two options: (a) include the FA4 shim/fontawesome.css compatibility layer, or (b) fork the iconset and prepend the right prefix — the picker's icon/template option lets you control the emitted class. Budget an afternoon for (b); it's a small fork, but it's your fork now..popover() signature (functions vs. options object). If you're on BS5, expect to patch or to run Bootstrap 4 alongside it on admin pages only. There is no maintained BS5 port from this repo.overflow: hidden cards. The dropdown renders inside the input's parent by default and gets cut off. Fix with z-index CSS on .iconpicker-popover and position: fixed-friendly placement (placement: 'bottom'), or move the render target to body. Verify in an actual modal before shipping.iconpickerSelected payload is not the input value. Read e.iconpickerValue, not $(this).val() — the input value is only synced by the plugin itself. Mixing these up yields off-by-one-icon bugs where the preview lags a click behind..iconpicker is not a namespaced selector; if your own CSS defines .iconpicker, you'll fight the widget's styles. Scope your field wrapper (.js-icon-field .iconpicker) and use a distinct init selector.if (!$(el).data('iconpicker')) — or call .iconpicker('destroy') first. Duplicate popovers stacked invisibly behind each other are a classic symptom.iconset, search is genuinely useful. With the full FA4 set plus a collapsed-by-default layout, users will say search "doesn't find" icons that exist — usually because the iconset was truncated.import $ from 'jquery' in an ES module gives you a module-scoped jQuery; the plugin attaches to window.jQuery. Always assign window.$ = window.jQuery = $ before importing the plugin, or use resolve.alias / optimizeDeps.include to guarantee a single instance.iconset also lets you drop the FA4 stylesheet from the admin bundle and ship only what the picker renders.dist/ into resources/ and pin it) or replace it. If your Laravel app leans heavily on Livewire/Alpine and FA6, a 100-line custom icon-select Blade component backed by a JSON icon list will usually be less work than keeping this alive. Adopt it where the Bootstrap 3/4 + FA4 combination already exists, not as a greenfield choice in 2024+.How can I help you explore Laravel packages today?