usamamuneerchaudhary/filament-command-palette
A Spotlight/CMD+K style command palette for Filament panels. Press Cmd+K (Mac) or Ctrl+K (Windows/Linux) to open, then jump to any page, resource, or navigation item with fast search and keyboard navigation. Includes an optional topbar button for click-to-open access.
Two commands and one line in a panel provider.
composer require usamamuneerchaudhary/filament-command-palette
php artisan vendor:publish --tag=command-palette-config
Register the plugin on the specific panel that needs it, not in a shared service provider — the palette resolves navigation per-panel, so scoping it keeps your public-facing panels clean:
use Usamamuneerchaudhary\CommandPalette\FilamentCommandPalettePlugin;
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->plugins([
FilamentCommandPalettePlugin::make(),
]);
}
No migrations, no new Vite entry, no published assets — it rides Filament's existing Alpine/Livewire bundle and inherits your panel's theme and dark mode.
Where to look first:
config/command-palette.php — this is essentially the package's entire API surface: key_bindings, show_topbar_button, max_results, template, templates, include_publish_views_command, custom_commands.Usamamuneerchaudhary\CommandPalette\Support\CommandItem — the fluent value object you'll construct for every custom entry.resources/views/vendor/command-palette/ — only appears after you publish views; that's your only styling escape hatch.First use case: ship it with zero custom commands and let one power user loose on it. Jumping to any page/resource/nav item is already the win on a panel with a deep sidebar, and it costs a single array entry. Count how often the palette actually gets opened before you invest in custom commands.
Day-two use case: the five actions your ops team repeats all day — "Create Invoice", "Open Overdue Invoices", "Switch to Tenant X" — wired up as CommandItems so they're two keystrokes away instead of four clicks deep.
boot(), not in the config fileconfig/command-palette.php ships a custom_commands array of closures, and the README demonstrates exactly that. It works locally and fails the moment you run php artisan config:cache. Inject at runtime instead:
// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Config;
public function boot(): void
{
Config::set('command-palette.custom_commands', [
fn () => app(BillingCommands::class)->all(),
fn () => app(AdminCommands::class)->all(),
]);
}
Provider boot() runs after cached config is loaded and before any panel renders, so the palette sees your commands in every environment.
use Usamamuneerchaudhary\CommandPalette\Support\CommandItem;
CommandItem::make('Edit User', '/admin/users/1/edit')
->group('Users')
->description('Administration')
->preview(['Email' => '[email protected]', 'Role' => 'Admin']);
description() and preview() are only consumed by the preview template — on standard or groups they're dead weight in every payload. Decide the template first, then write the builders.
The palette is a launcher, not an authorizer. It lists whatever your closure returns; the destination URL enforces policy. That means an unauthorized user sees the entry, clicks it, and eats a 403 — poor UX and a minor information leak about what exists.
final class BillingCommands
{
public function all(): array
{
if (! auth()->user()?->can('manage_billing')) {
return [];
}
return [
CommandItem::make('Create Invoice', route('filament.admin.resources.invoices.create')),
CommandItem::make(
'Overdue Invoices',
'/admin/invoices?tableFilters[overdue][isActive]=1',
'Billing',
),
];
}
}
Closures execute per-request with auth() available, so this works — just never memoise the result across users.
Anonymous closures in a provider are untestable and awkward to delete later. A small BillingCommands / AdminCommands pair buys you unit tests and a one-file removal path:
it('exposes invoice commands to permitted users', function () {
$this->actingAs(User::factory()->create(['role' => 'billing']));
$items = app(BillingCommands::class)->all();
expect($items)->not->toBeEmpty()
->and($items[0])->toBeInstanceOf(CommandItem::class);
});
You can't assert on an Alpine modal without a browser test, but you can absolutely assert that your command list is valid, permission-filtered, and non-empty.
Hardcoded /admin/... strings in the README examples will break as soon as a panel uses a different path(), and if you use Filament tenancy they'll also miss the tenant slug and land on a 404. Prefer route() / Resource::getUrl() with the tenant passed through, and verify each custom command in the target environment rather than copy-pasting from the README.
'template' => 'standard',
'show_topbar_button' => env('COMMAND_PALETTE_TOPBAR', true),
'include_publish_views_command' => env('APP_ENV') !== 'production',
'max_results' => 10,
template is global, not per-panel — pick one and commit. preview is the best demo and the most maintenance: every command you care about needs real description()/preview() data or the right pane renders as a stub. Under ~15 well-described custom commands, standard is the honest choice.
php artisan config:cache var_exports the config array; a closure becomes Closure::__set_state(...) and the app dies on boot with Call to undefined method Closure::__set_state(). Symptom is usually "works locally, fatal in staging" immediately after a deploy. Always inject custom_commands at runtime and add config:cache && config:clear to CI.mod+k collides with Filament's global search. Filament binds Cmd/Ctrl+K to its own search modal. If the panel has global search enabled you get two listeners fighting over one keystroke. That's what show_topbar_button_when_global_search_enabled is for — but the real fix is deciding which one wins and remapping the other via key_bindings.include_publish_views_command in production. It exposes a developer action that writes view files into your deployed app, to whatever user can open the palette. There is no end-user value. Set it false on anything customer-facing.php artisan vendor:publish --tag=command-palette-views (or command-palette:publish-views, or the in-palette shortcut) copies templates into resources/views/vendor/command-palette/, including livewire/templates/ per layout. Release 1.4 explicitly reworked the templates, so anything published pre-1.4 is now divergent. Publish only for a concrete styling requirement, and re-diff after every upgrade.max_results is per category/group, not global. Default 10. Verify whether it also truncates your custom-command groups before you file it as a bug — and lower it if noisy commands are pushing real nav items out of view.SELECT * FROM orders inside one turns a keystroke into a query. Cache read-only lookups at boot, never at render.composer.json, read the tag's own composer.json for Filament/Laravel constraints (the README says Filament v5 / Laravel 11–12 while 1.1 added v4 and 1.3 added Laravel 13 — the docs and the constraint matrix drift), and treat upgrades as a review, not a composer update.[] silently looks identical to broken); (4) does the command URL resolve with the correct panel prefix/tenant. Nothing here logs — a silently empty palette is nearly always (2) or (3).CommandItem should point at a URL a user can still reach from normal navigation.How can I help you explore Laravel packages today?