usamamuneerchaudhary / filament-command-palette
A Spotlight/CMD+K style command palette for Filament: fuzzy search across navigation, records and actions.
Package info
github.com/usamamuneerchaudhary/filament-command-palette
pkg:composer/usamamuneerchaudhary/filament-command-palette
Fund package maintenance!
Requires
- php: ^8.2
- filament/filament: ^4.0|^5.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- spatie/laravel-package-tools: ^1.19
Requires (Dev)
- larastan/larastan: ^2.9|^3.0
- laravel/pint: ^1.17
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
- usamamuneerchaudhary/laravel-slipway: ^1.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Press ⌘K and go anywhere in your panel — a page, a record, or an action — without touching the sidebar.
Features
- Fuzzy search.
crusfinds "Create User". Matched characters are highlighted. - Record search. Type an invoice number, land on the invoice. Built on each resource's existing Filament global search configuration, so most apps get this with no extra setup.
- Actions, not just links. Run a server closure or fire a browser event. Sign out, switch theme, clear a cache.
- Authorization aware. Commands are filtered by the same policies Filament uses, and every action is re-authorized on the server before it runs.
- Learns what you use. Recently run commands rank higher and appear first on an empty search.
- Search modes.
>for actions,#for records,?for the shortcut list. - Keyboard and screen reader ready. Full combobox semantics, focus trapping, focus restoration, and result announcements.
- Per panel. Every option can differ between panels in the same application.
- Three layouts.
standard,groups,preview.
Requirements
- PHP 8.2+
- Laravel 11, 12 or 13
- Filament v4 or v5
Installation
composer require usamamuneerchaudhary/filament-command-palette
Register the plugin on your panel:
use Usamamuneerchaudhary\CommandPalette\FilamentCommandPalettePlugin; public function panel(Panel $panel): Panel { return $panel->plugins([ FilamentCommandPalettePlugin::make(), ]); }
That is the whole setup. The palette picks up your navigation, pages and resources automatically.
Configuration
Options can be set on the plugin, per panel:
FilamentCommandPalettePlugin::make() ->keyBindings(['mod+k']) ->template('groups') ->maxResults(12) ->recordSearch(true, maxResults: 20) ->recents(true, max: 5) ->topbarButton(false) ->cacheFor(60)
Or globally, in the published config file:
php artisan vendor:publish --tag=command-palette-config
Plugin options win over the config file, so the config sets your defaults and each panel adjusts from there.
maxResults() / max_results is a total cap on the ranked list, not a per-category limit. Recents are capped separately by recents.max (5 by default). Record search has its own record_search.max_results.
Key bindings
mod means Cmd on macOS and Ctrl elsewhere. If Filament's own global search is bound to a shortcut, give the two different keys:
$panel ->globalSearchKeyBindings(['mod+shift+k']) ->plugins([ FilamentCommandPalettePlugin::make()->keyBindings(['mod+k']), ]);
Custom commands
Register commands on the plugin:
use Usamamuneerchaudhary\CommandPalette\Support\CommandItem; FilamentCommandPalettePlugin::make() ->commands(fn () => [ CommandItem::make('Billing portal', 'https://billing.example.com') ->group('External') ->openInNewTab() ->keywords(['invoice', 'subscription', 'payment']), ])
Register commands here rather than in the
custom_commandsconfig key. Config files are serialized byphp artisan config:cache, and closures cannot survive that — it fails with "Your configuration files are not serializable". The config key still works, but the plugin is the right home for anything with a closure in it.
Running an action
Return a string to redirect afterwards:
CommandItem::make('Clear application cache') ->icon(Heroicon::OutlinedTrash) ->action(function () { Artisan::call('cache:clear'); Notification::make()->title('Cache cleared')->success()->send(); }) ->authorize(fn () => auth()->user()->isAdmin())
The browser only ever sends the command's id. The closure, the URL and the authorization check are all resolved server-side from a freshly built registry, so a stale or tampered payload cannot run anything.
Firing a browser event
For anything that lives purely in the client:
CommandItem::make('Open help')->dispatch('open-help-drawer', ['section' => 'billing'])
Controlling visibility
CommandItem::make('Impersonate user', '/impersonate') ->visible(fn () => config('app.debug')) ->authorize(fn () => auth()->user()->can('impersonate'))
Or gate everything at once:
FilamentCommandPalettePlugin::make() ->authorize(fn (CommandItem $item) => ! str_starts_with($item->group, 'Internal'))
Record search
Any resource that supports Filament's global search is searchable from the palette:
class OrderResource extends Resource { protected static ?string $recordTitleAttribute = 'reference'; public static function getGloballySearchableAttributes(): array { return ['reference', 'customer.name']; } public static function getGlobalSearchResultDetails(Model $record): array { return [ 'Customer' => $record->customer->name, 'Total' => $record->total_formatted, ]; } }
Queries are debounced, run on the server, and respect the same scoping and policies as everywhere else in Filament. Details show up in the preview template's side pane.
To turn it off: ->recordSearch(false).
Custom providers
The command list is produced by a pipeline. Add your own stage:
use Usamamuneerchaudhary\CommandPalette\Contracts\CommandProvider; class DocumentationCommands implements CommandProvider { public function commands(Panel $panel): iterable { return collect(Documentation::published())->map( fn ($doc) => CommandItem::make($doc->title, $doc->url)->group('Docs') ); } }
FilamentCommandPalettePlugin::make() ->providers([...CommandRegistry::DEFAULT_PROVIDERS, DocumentationCommands::class])
Implement SearchesCommands instead if your results depend on the search term.
Events
use Usamamuneerchaudhary\CommandPalette\Events\CommandExecuted; Event::listen(CommandExecuted::class, function (CommandExecuted $event) { Log::info('palette', ['command' => $event->command['label'], 'query' => $event->query]); });
CommandPaletteOpened fires the first time someone opens it in a page session. Between them you can find out which commands actually earn their place.
Translations
php artisan vendor:publish --tag=command-palette-translations
Every string is translatable. Layouts use logical properties, so RTL panels lay out correctly without further work.
Customising the views
php artisan command-palette:publish-views
The publish-views page is registered by default, but canAccess() is local-only, so it never appears outside local. Set include_publish_views_command to false to hide it entirely.
Most appearance changes do not need this — the palette is styled entirely through CSS custom properties:
.fi-cp-root { --fi-cp-radius: 0.25rem; --fi-cp-selected-accent: rgb(var(--danger-600)); }
Published views take precedence over the package's from then on, which also means future package updates will not reach them. Prefer the variables where you can.
Performance
The palette ships an empty shell with the page and fetches its commands the first time it opens. Nothing is embedded in every page response.
If building the list is expensive and your navigation is static, cache it:
FilamentCommandPalettePlugin::make()->cacheFor(300)
Cache keys include the panel, user, tenant and locale, so nothing leaks between users.
Upgrading from v1
See UPGRADE.md. The short version: move custom_commands closures onto the plugin, and check any code that constructed CommandItem directly.
Testing
composer test composer analyse composer format composer slipway:compile # regenerate .github/workflows/slipway.yml after changing slipway.php
Credits
License
MIT. See LICENSE.md.
