Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Command Palette Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. 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.
  2. Usamamuneerchaudhary\CommandPalette\Support\CommandItem — the fluent value object you'll construct for every custom entry.
  3. 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.

Implementation Patterns

Register custom commands in a provider's boot(), not in the config file

config/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.

Build items with the fluent API, and skip the metadata you don't need

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.

Gate commands inside the closure, every request

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.

Extract command providers into invokable classes

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.

Build URLs with route helpers, and check your panel path prefix

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.

Env-scope the config in one place

'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.

Gotchas and Tips

  • Closures in cached config blow up. 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.
  • Turn off 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.
  • Publishing views is a fork. 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.
  • No record search out of the box. The package blurb mentions records, but the documented feature set is navigation, resources and pages. If you need "find invoice #4471", build it as custom commands or lean on Filament's global search; don't assume the palette indexes your models.
  • Keep the closures cheap. Custom command builders run on palette render. A SELECT * FROM orders inside one turns a keystroke into a query. Cache read-only lookups at boot, never at render.
  • Small package, zero dependents. 19 stars, single maintainer, one contributor PR merged in 1.3. Pin the exact version in 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.
  • Debugging checklist when the palette misbehaves: (1) is the plugin registered on this panel; (2) is config cached from a stale deploy; (3) does the current user pass your closure's permission gate (returning [] 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).
  • Keep the removal path open. The whole integration should be: delete the plugin line, delete your command classes, delete the config. Never let a custom command become the only route to an action — every CommandItem should point at a URL a user can still reach from normal navigation.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table