charrafimed/global-search-modal
Add a polished global search modal to your Filament panel. Search resources and pages instantly with a keyboard shortcut, customizable results, and a fast command-palette experience that feels native to Laravel and Filament.
Install and register
composer require charrafimed/global-search-modal
Register the plugin on the panel you want it on (not globally, unless you want it everywhere):
use Charrafimed\GlobalSearchModal\GlobalSearchModalPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(GlobalSearchModalPlugin::make());
}
Then publish the config and (if the package ships them) the assets:
php artisan vendor:publish --tag="global-search-modal-config"
php artisan filament:assets
php artisan view:clear
Where to look first
config/global-search-modal.php — this is where almost every behavioural toggle lives (shortcut key, modal width, debounce, result limits, whether the default topbar input is hidden). Read this before touching anything else; most "how do I…" questions are answered here rather than in code.GlobalSearchModalPlugin) — the fluent methods are the supported extension surface. Anything you can't configure here is usually a publish-and-override job.resources/views/ in the package — the modal Blade component. Publish views (--tag="global-search-modal-views") only when you genuinely need to change markup.getGloballySearchableAttributes(), getGlobalSearchResultTitle(), getGlobalSearchResultDetails(), getGlobalSearchResultUrl(). The modal is only a shell; result quality is entirely determined by these.First use case
Enable the plugin, hit ⌘K / Ctrl+K (or click the search field), type the name of a record, arrow through results, hit Enter. Verify three things on day one: (a) the shortcut opens the modal from any panel page, (b) results are scoped to what the current user is authorised to see, (c) clicking a result navigates and the modal closes cleanly — including under SPA/Livewire navigation.
If (a) fails, it's almost always a stale asset cache or a conflicting shortcut from another plugin. If (b) fails, it's a policy issue on your Resources, not the package.
Keep configuration in the panel, not in a service provider. The plugin is a panel plugin; config published to config/ is a global default, but per-panel overrides via the fluent API let you run a leaner search on an admin panel and a stricter one on a customer-facing panel.
->plugin(
GlobalSearchModalPlugin::make()
->shortcut('mod+k') // check exact method names in the plugin class
->modalWidth('2xl')
->resultsPerSection(5)
)
Programmatic opening. The modal is Alpine/Livewire driven, so you can trigger it from your own UI — a topbar action, an onboarding tooltip, a "Search" button in a custom page. Use the browser event the package dispatches (inspect the Blade view for the exact event name, typically something like open-global-search-modal) rather than reaching into component internals:
<x-filament::button
x-on:click="$dispatch('open-global-search-modal')"
icon="heroicon-o-magnifying-glass">
Search
</x-filament::button>
This is the cleanest integration point and survives package upgrades better than overriding the component.
Shape results for a modal, not a dropdown. The default Filament global search dropdown shows ~5 results per resource and truncates details. In a modal you have vertical space, so:
getGlobalSearchResultDetails() (status, owner, email) since the modal has room to render it.getGlobalSearchResultTitle() for the primary line and keep it short; long titles wrap awkwardly in the list.getGlobalSearchResultActions() if you want "Edit" / "View" affordances directly in the result row.public static function getGloballySearchableAttributes(): array
{
return ['name', 'email', 'company.name'];
}
public static function getGlobalSearchResultDetails(Model $record): array
{
return [
'Company' => $record->company?->name,
'Status' => $record->status?->label(),
];
}
Gate the search per user. Don't rely on the modal to hide anything. Implement canGloballySearch() on sensitive Resources and canView() on the model policy. The modal renders whatever Filament's search layer returns — if a record leaks into global search, it leaks into the modal.
Eager-load for the modal. Because the modal renders more results per resource than the dropdown, relationship access in getGlobalSearchResultDetails() that was previously fine can become an N+1. Override getGlobalSearchEloquentQuery() and with() the relations you touch:
public static function getGlobalSearchEloquentQuery(): Builder
{
return parent::getGlobalSearchEloquentQuery()->with(['company', 'status']);
}
Custom theme integration. If you use a custom Filament theme, add the package views to your Tailwind content paths so its classes aren't purged:
// tailwind.config.js
content: [
'./vendor/charrafimed/global-search-modal/resources/views/**/*.blade.php',
// ...
]
Then npm run build. Skipping this is the single most common cause of "the modal renders unstyled" reports.
Multi-panel and tenancy. In a tenanted panel, global search is already scoped by the tenant relationship; verify the modal respects it by logging in as a user from tenant A and searching for a tenant B record. If the package offers a "recent searches" or history feature, confirm it doesn't persist across tenants.
Testing. Cover the three things that break: shortcut opens the modal (Dusk/Playwright is the pragmatic choice — Alpine keybindings are painful to assert in Livewire tests), results are policy-scoped (plain Livewire assertDontSee), and navigation closes the modal.
Tailwind purging is the #1 support issue. Published assets + custom theme = classes stripped. Always add the vendor view path to content and rebuild. If you're not using a custom theme, run php artisan filament:assets after every composer update, otherwise you'll be running last version's compiled CSS against this version's markup.
Shortcut collisions. ⌘K/Ctrl+K is a popular binding — the browser's own address-bar search on some platforms, command palettes in other Filament plugins, and OS-level tooling all compete. If two plugins both claim it, one silently wins (usually whichever registers last, which is not the order you wrote them in). Test with all your plugins installed together and pick a non-default binding if there's contention.
Don't edit vendor files. The temptation with a modal is to tweak the Blade markup directly. Publish the views and override there, or you'll lose the change on the next composer update — and this package is actively released, so updates will happen.
Version pinning matters more than usual here. This is a Filament plugin wrapping internals that move between Filament majors (the topbar, the search component, and the Alpine stores all changed shape between v3 and v4). Pin both filament/filament and this package in composer.json, and read the release notes before bumping either. A plugin whose latest release is a date far in the future relative to your Filament version is a warning sign, not a reassurance — check the composer.json constraints in vendor/charrafimed/global-search-modal for what it actually supports.
Debounce and query cost. Global search fires on keystroke. With getGloballySearchableAttributes() including relationship columns (company.name), every keystroke becomes a join. Either keep the attribute list flat or raise the debounce in config. Watch the query log while typing three characters — if you see double-digit query counts, that's the cause, and it gets worse in the modal because more results render.
Search results respect getGlobalSearchResultUrl(), not your table action. If clicking a result takes you somewhere unexpected, the fix is in the Resource, not the package. Same for shouldOpenGlobalSearchResultUrlInNewTab() if you want cmd-click behaviour.
Authorization is not the plugin's job. There's a subtle failure mode where canGloballySearch() returns true but the model policy denies view — the record appears in the modal and 403s on click. Audit that pairing per Resource; the modal makes leaks more visible, it doesn't create or prevent them.
Dark mode and modal stacking. The modal uses Filament's design tokens, so it inherits dark mode automatically — unless you've overridden colours in a published view with hardcoded hex values. Use fi- utility classes and CSS variables. Separately, if you open the modal from inside another Filament modal (e.g. a relation manager), check z-index and focus trapping; nested Alpine focus traps are a known source of "keyboard stops working" bugs.
Debugging a modal that won't open. In order: (1) view:clear + filament:assets, (2) hard refresh to bust the browser cache, (3) check the browser console for an Alpine error — usually a duplicate x-data registration from another plugin, (4) confirm the plugin is registered on this panel, not just the default one, (5) php artisan about to confirm the package is discovered.
Extension points, ranked by safety. Fluent config on the plugin > dispatching/listening to browser events > published views > published config > subclassing the plugin. Anything below published config will be re-broken by the next release; budget for it accordingly.
How can I help you explore Laravel packages today?