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

Actions Laravel Package

filament/actions

Filament Actions provides a fluent API for adding interactive buttons, links, and icon buttons to Laravel Filament apps. Define confirmation modals, forms, notifications, redirects, and authorization in one place, then reuse actions across tables, pages, and resources.

View on GitHub
Deep Wiki
Context7

Getting Started

Understand what you're actually installing. filament/actions is not a standalone modal library — it is the action engine that powers every button, confirmation dialog, and modal form inside Filament panels. Installing it pulls in filament/support, filament/forms, filament/infolists, filament/notifications, and Livewire. If you already run a panel you have it; you're only "adding" it if you want actions outside a panel.

1. Install and confirm the version.

composer require filament/actions
composer show filament/actions   # note 3.x vs 4.x — the API differs

2. Wire it into a Livewire component (skip this entirely if you're inside a panel — resource and table actions are already available):

use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Livewire\Component;

class ManageOrders extends Component implements HasActions
{
    use InteractsWithActions;

    // ...
}
<div>
    {{ $this->actions }}

    <x-filament-actions::modals />
</div>

Outside a panel you also need Filament's assets: @filamentStyles in <head> and @filamentScripts before </body> (or your own Tailwind build scanning vendor/filament/**/*.blade.php). The modal component must render exactly once per page — that is the most common first-hour failure.

3. First use case: a destructive confirmation. This is the highest value-per-line action you can ship, and it's copy-pasteable from any generated resource:

use Filament\Actions\Action;

public function deleteAction(): Action
{
    return Action::make('delete')
        ->label('Delete order')
        ->icon('heroicon-o-trash')
        ->color('danger')
        ->requiresConfirmation()
        ->modalHeading('Delete this order?')
        ->modalDescription('Line items will be archived. This cannot be undone.')
        ->modalSubmitActionLabel('Yes, delete')
        ->action(function (): void {
            $this->order->delete();
        })
        ->success();
}

Where to look first, in order:

  1. Filament\Actions\Action itself — read the Concerns/ traits it composes (CanBeHidden, HasForm/HasSchema, CanBeAuthorized, HasAction, HasLifecycleHooks). The method list in the docs is a summary; the traits are the real API.
  2. A resource generated with php artisan make:filament-resource --generate — look at table() and the EditAction/DeleteAction/ViewAction it wires up. That is the canonical pattern.
  3. The docs page for Actions, then Actions → Testing.

Budget a day for a Livewire-comfortable developer to be productive; two hours if they've used Filament tables before.

Implementation Patterns

Class-based actions are the default once you have more than two. Inline Action::make() closures sprawl. Promote to a class and configure in setUp():

namespace App\Filament\Resources\Orders\Actions;

use Filament\Actions\Action;

class RefundOrder extends Action
{
    protected function setUp(): void
    {
        parent::setUp();

        $this
            ->label('Refund')
            ->icon('heroicon-o-banknotes')
            ->requiresConfirmation()
            ->schema([
                TextInput::make('amount')->numeric()->required(),
                Textarea::make('reason')->required(),
            ])
            ->action(function (array $data, array $arguments): void {
                $order = Order::findOrFail($arguments['record']);
                $order->refund($data['amount'], $data['reason']);
            })
            ->successNotificationTitle('Refund issued');
    }
}

Then reuse it everywhere with a different record: table row, table header, page header, relation manager — RefundOrder::make()->record($order). One definition, four surfaces is the whole point of this package.

Dependency injection in handlers. The closure signature is resolved, not positional:

->action(function (array $data, Action $action, Model $record, Component $livewire) {
    if (! $record->isRefundable()) {
        $action->halt();      // stop, keep the modal open, don't fire success
    }
})

$data is the validated form state; $arguments carries anything passed to mountAction(). Name them — never $a, $d.

Programmatic control. For flows where a button starts the sequence but something else finishes it:

$this->mountAction('refund', arguments: ['record' => $order->id]);
$this->callMountedAction();     // fires the mounted action's handler
$this->getMountedAction();      // inspect the live instance
$this->unmountAction();         // close without running

This is how you build "confirm, then redirect to checkout" or chain a second modal from the first one's ->after().

Lifecycle hooks instead of stuffing logic into action(). ->before() runs on click, ->after() on success, beforeFormValidated() / afterFormValidated() bracket validation. Use ->after(fn () => $this->dispatch('order-refunded')) to tell the rest of the page to refresh, rather than doing view-side bookkeeping.

Bulk work. On v3 this is BulkAction; on v4 the classes were unified under Action::make()->bulk(). Either way the handler gets a Collection:

Action::make('archive')
    ->bulk()
    ->requiresConfirmation()
    ->action(fn (Collection $records) => $records->each->archive())
    ->deselectRecordsAfterCompletion()
    ->success();

For large selections, chunking and ->fetchSelectedRecords() matter — don't ->each() 50k rows in one request.

Read-only and rich modal bodies. Not every modal is a form. For an order-preview modal, pass a Blade view instead:

->modalContent(fn (Order $record) => view('orders.preview', ['order' => $record]))
->modalSubmitAction(false)
->modalCancelActionLabel('Close')

and pair it with an Infolist inside that view if you want structured output.

Multi-step modals. ->steps([Step::make('Details')->schema([...]), Step::make('Confirm')->schema([...])]) gives you a wizard in the modal, with each step validated before advancing. Prefer this over chaining two modals — state survives, back navigation works.

Testing is the killer feature — use it. Actions are fully testable without a browser:

it('refunds an order', function () {
    $order = Order::factory()->create();

    livewire(ManageOrders::class, ['order' => $order])
        ->callAction(RefundOrder::class, arguments: ['record' => $order->id])
        ->assertActionMounted(RefundOrder::class)
        ->fillForm(['amount' => 50, 'reason' => 'Damaged'])
        ->callMountedAction()
        ->assertHasNoActionErrors()
        ->assertNotified();

    expect($order->fresh()->refunded_at)->not->toBeNull();
});

assertActionExists, assertActionHidden, assertActionVisible, assertActionDisabled cover the UI matrix cheaply. For every destructive action, write two tests: the authorized happy path and an actingAs user without the policy ability. The second one is the test people skip and the one that catches real bugs.

Team-wide conventions via configureUsing(). Instead of repeating ->modalWidth('2xl') and ->slideOver() on every action, set defaults once in a service provider:

Action::configureUsing(fn (Action $action) => $action->modalWidth('lg'));

Add Action::macro('asDangerous', ...) for in-house sugar. This is the sanctioned extension point — prefer it over subclassing everything.

Gotchas and Tips

Version drift will silently break your copy-paste. The v4 release renamed the modal form API from ->form([...]) to ->schema([...]), merged BulkAction into Action, and reworked success/failure handling. Most blog posts, Stack Overflow answers, and LLM output are still v3. When a method "does nothing", check composer show filament/actions before debugging anything else — a wrong-version call often no-ops rather than erroring. Pin filament/* and livewire/livewire together.

<x-filament-actions::modals /> once, and only once. Missing it → buttons click, nothing opens. Rendering it twice (e.g. including a partial that contains it on every card in a loop) → duplicate modal IDs and Alpine state fighting itself, where the second modal opens behind the first. Same trap with {{ $this->actions }}: render it per component, not per row.

Visibility is not authorization. ->visible() and ->hidden() are UI. The handler is reachable via a crafted Livewire request if your component's action set is ever resolved loosely. Always gate inside the handler:

->authorize('refund', $record)          // hides/disables per policy
->action(function (Action $action, Order $record) {
    abort_unless(auth()->user()->can('refund', $record), 403);
    // ...
})

Note that authorize() semantics (hidden vs. throwing vs. showing a message) have shifted between v3 and v4 — assert the behavior you expect instead of assuming it.

Actions are evaluated on every render. getActions() / the Action object array is rebuilt each Livewire render. Heavy work inside ->visible(fn () => $record->load('lines')->isRefundable()) on a 50-row table is 50 queries per keystroke. Make visibility checks cheap (a boolean column, a scopes-loaded attribute) and push expensive reads into the handler or the modal body.

halt() vs cancel() vs exceptions. $action->halt() stops execution and keeps the modal mounted — use it for business-rule rejections after validation. $action->cancel() closes without running ->after(). Throwing lets Livewire surface the exception (fine in tests, ugly in prod). Validation failures halt automatically; don't double-handle them.

Redirecting from inside an action. Returning a redirect from the handler can leave a null-rendered modal behind on slow connections. Prefer ->successRedirectUrl(route('orders.index')) — it tears the modal down in the right order.

wire:key in any manual loop. If you render <x-filament::button> or action buttons inside a @foreach over a custom collection, give each a stable key. Without it, Livewire's DOM diffing reuses the wrong Alpine scope and you get the classic "click row 3, row 1's modal opens."

File uploads in action forms need the disk configured. config/filament.php → default_filesystem_disk must point at a real disk, and temp uploads need the livewire config. Missing it looks like a silent no-op on submit rather than a visible error. Also: uploads inside a modal survive the Livewire round trip only if the form component is actually mounted — don't swap ->schema() for a manually rendered partial mid-flow.

Debugging the modal state. Livewire's browser devtools show the component payload; look for the mountedActions array — if your action appears with empty arguments, the problem is at mountAction(), not in the handler. logger() / ray() inside the closures works normally. If an action is missing entirely, check in this order: visible(), authorize(), the policy, then whether the array key is being overwritten by a same-named action on another surface (table header vs row actions resolve from different arrays but share the Action class if you reused one carelessly).

Bulk actions don't fetch records by default in every configuration. On large tables, $records may contain only the IDs from the current page. Call ->fetchSelectedRecords() explicitly if your handler needs full models, and test with "select all matching" — not just the checkboxes you clicked.

Don't fight the design system. The package's value is consistency. Reaching for custom Alpine to restyle a modal usually means the requirement belongs in a bespoke Livewire component instead. Keep escape hatches honest: ->modalContent() for bespoke bodies, a plain Livewire component for genuinely unusual flows — and document that the two coexist.

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.
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections