Core
Action Modals
What an action asks before it runs — a sentence, a form, a read-only infolist or a wizard — and what happens when one modal opens another.
On this page
An action can ask before it runs, and what it asks with is a modal: a sentence and two buttons, a form to fill in, a read-only infolist to check, or a wizard of steps. All four are the same surface configured differently — none of them is a second kind of action, and the callback at the end is the one you already wrote.
Confirmation Modal
Action::make('delete') ->requiresConfirmation() ->modalHeading('Delete this record?') ->modalDescription('This action cannot be undone.') ->modalIcon('trash', 'danger') ->modalSubmitActionLabel('Yes, delete') ->modalCancelActionLabel('Cancel') ->action(fn ($record) => $record->delete());
Slide-Over
Action::make('details') ->slideOver() ->stickyHeader() ->stickyFooter() ->modalMaxHeight('60vh');
Modal Appearance
Action::make('edit') ->modalWidth('2xl') // sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl ->closeModalOnClickAway() ->closeModalOnEscape() ->slideOverOnMobile() // slide-over on mobile, modal on desktop ->fullScreenOnMobile(); // full screen on mobile
Form Modal
When wire-forms is installed, actions can display form modals:
use NyonCode\WireForms\Components\TextInput;use NyonCode\WireForms\Components\Select; Action::make('edit') ->form([ TextInput::make('name')->required(), Select::make('role')->options([ 'admin' => 'Admin', 'editor' => 'Editor', ]), ]) ->fillFormUsing(fn ($record) => $record->only(['name', 'role'])) ->action(fn ($record, array $data) => $record->update($data));
The closure may hand back an enum case straight off a cast attribute (fn ($record) => ['role' => $record->role]): the seeded bag collapses every enum to its backing value, because that is what Livewire state carries to the browser and what a Select matches its <option> values against. The choice still saves back through the cast.
$data arrives dehydrated, exactly as Form::save() would have written it: a cleared Select or emptied numeric TextInput is null rather than '', a DateTimePicker carries its storage format and timezone, a FileUpload a stored path, and any dehydrateStateUsing() you declared has been applied. That is why $record->update($data) above is safe against an enum or numeric cast. See what reaches the record.
A HeaderAction form modal has no record, so its fillFormUsing closure takes no arguments. Use it to seed initial state — and always seed array-typed fields (CheckboxList, Tags, multiple Select) with an empty array so they bind correctly from the first interaction:
HeaderAction::make('create') ->form([ TextInput::make('name')->required(), CheckboxList::make('permissions')->options($permissions)->bulkToggleable(), ]) ->fillFormUsing(fn () => ['name' => '', 'permissions' => []]) ->action(fn (array $data) => Role::create($data));
Infolist Modal
Use ->infolist() to open a read-only modal that displays the record — the counterpart of ->form(). The action's record is bound automatically, the modal is not a confirmation, and it shows only a close button (no submit). See Infolists for the full entry reference.
use NyonCode\WireCore\Actions\ViewAction;use NyonCode\WireCore\Infolists\Components\TextEntry; ViewAction::make() ->slideOver() ->infolist([ TextEntry::make('name')->weight('bold'), TextEntry::make('email')->copyable(), TextEntry::make('created_at')->dateTime()->since(), ]);
Multi-Step Wizard
use NyonCode\WireCore\Actions\ModalStep; Action::make('create') ->steps([ ModalStep::make('Basic Info') ->description('Enter user details') ->icon('user') ->schema([ TextInput::make('name')->required(), TextInput::make('email')->email()->required(), ]), ModalStep::make('Settings') ->schema([ Select::make('role')->options([...]), Toggle::make('active'), ]), ModalStep::make('Review') ->schema([ Placeholder::make('summary'), ]), ]) ->action(fn ($record, $data) => $record->update($data));
A step's ->schema() accepts a Closure to build its fields from data entered in
earlier steps — ->schema(fn (array $data) => [...]). The Closure receives the live
form-data bag even for HeaderAction (which has no record). See
Multi-Step Wizard for a worked example.
Footer Actions
use NyonCode\WireCore\Actions\ModalFooterAction; Action::make('edit') ->form([...]) ->modalFooterActions([ ModalFooterAction::make('save') ->label('Save') ->color('primary') ->submitsForm(), ModalFooterAction::make('save-and-close') ->label('Save & Close') ->action(fn () => $this->saveAndClose()), ]);
Stacked (Nested) Modals
Opening an action while a modal is already open stacks the new modal on top of
the current one instead of replacing it. Every open modal is a live frame: the
parent stays a fully reactive form behind the active one (dimmed and click-inert,
but still re-rendering), and closing the top modal returns you to the parent with its
form data intact. There is no special API — any callback that receives the host
$component (a footer action, a field action, an infolist action) can open another
action, and it just stacks:
Action::make('editOrder') ->modalHeading('Edit order') ->form([ TextInput::make('reference')->required(), Select::make('customer_id')->options($customers), ]) ->modalFooterActions([ // Opens a second modal on top of "Edit order". The parent stays open // behind it; closing the child returns here with the form untouched. ModalFooterAction::make('newCustomer') ->label('New customer') ->icon('plus') ->action(fn ($component) => $component->mountAction('createCustomer')), ]) ->action(fn (array $data) => $this->saveOrder($data)); Action::make('createCustomer') ->modalHeading('Create customer') ->form([TextInput::make('name')->required()]) ->action(fn (array $data) => Customer::create($data));
Inside a table, open a nested modal from within an action the same way — the host
is passed as $component:
Action::make('review') ->modalHeading('Review') ->modalFooterActions([ ModalFooterAction::make('flag') ->label('Flag for follow-up') ->action(fn ($component, $record) => $component->openActionModal((string) $record->getKey(), 'addFlag')), ]);
The nested action can live in the top-level list, or be declared inline next
to the action that opens it with registerActions() — the resolver finds it by
name either way:
Action::make('editOrder') ->registerActions([ Action::make('createCustomer')->form([...])->action(...), ]) ->modalFooterActions([ ModalFooterAction::make('newCustomer') ->action(fn ($component) => $component->mountAction('createCustomer')), ]);
Returning data to the parent
Because every level is a live frame in one component, a nested action can write
straight back into an ancestor's form. Every action and footer callback receives
these bindings alongside the usual $data/$record/$component:
$set(path, value)— write your own frame's form data.$setParent(path, value)— write the parent frame's form data.$parentData— read the parent frame's current form data.$setFrame(depth, path, value)— write any frame by stack depth (power users).$arguments— the arbitrary array you passed tomountAction($name, [...]).
The stack is capped at ModalStack::MAX_DEPTH (8). It is a guard against
pathological re-entrancy — a callback that opens a modal in a loop — rather than
a limit anyone reaches by designing a flow: opening one more than that is refused,
not silently dropped.
This is the canonical "create + select" pattern — a sub-form that fills a field on the form that opened it:
Action::make('editOrder') ->form([ TextInput::make('reference')->required(), Select::make('customer_id')->options(fn () => Customer::pluck('name', 'id')), ]) ->modalFooterActions([ ModalFooterAction::make('newCustomer') ->label('New customer')->icon('plus') ->action(fn ($component) => $component->mountAction('createCustomer')), ]) ->action(fn (array $data) => $this->saveOrder($data)); Action::make('createCustomer') ->modalHeading('Create customer') ->form([TextInput::make('name')->required()]) // Create the record and hand its id back to the parent's Select, then close. ->action(function (array $data, $setParent) { $customer = Customer::create($data); $setParent('customer_id', $customer->id); });
The write re-renders the whole stack, so the parent Select shows the new value
the moment the child closes.
Behaviour notes:
- Stack as deep as you need — each level layers above the previous one with an
increasing
z-index; a single scrim covers everything beneath the top modal so a deep stack never darkens into black. (A safety cap guards against runaway re-entrancy.) - The parent stays live — only the top modal is interactive, but every parent below
it keeps re-rendering, so a
$setParent(...)write shows up behind the active modal immediately. - Close returns to the parent.
Escape, the close button, clicking the backdrop, or a footer action that closes the modal all pop just the top modal and resume the parent. The last close clears the stack. - Form data is preserved per level, so the parent modal is exactly as you left it.
- A footer action that opens a nested modal is not auto-closed afterwards, so the modal it opened stays on top.
Navigating the stack
Two more callback bindings compose deep flows without stacking another layer:
$replace(name, arguments = [])— swap the active modal for another in place. The current top is popped and the named action mounts at the same depth, so parents stay untouched. Use it to move within a modal — a "back to step one" button, or trading an edit modal for a confirm modal — instead of piling on a new level. A replaced row action's record is inherited automatically (passrecord/recordKeyinargumentsto override).$cancelParents(?upTo = null)— close the active modal and its parents. With no argument it dismisses the whole stack (one "Cancel all"); pass an action name to unwind up to and including the nearest ancestor with that name.
Action::make('editOrder') ->form([/* … */]) ->modalFooterActions([ // Swap this modal for a confirmation, in place — no extra layer. ModalFooterAction::make('archive') ->label('Archive…') ->action(fn ($replace) => $replace('confirmArchive')), // Abandon the entire nested flow at once. ModalFooterAction::make('discard') ->label('Discard all') ->action(fn ($cancelParents) => $cancelParents()), ]) ->action(fn (array $data) => $this->saveOrder($data));
Both are also public methods ($this->replaceMountedAction(...), $this->cancelParentActions(...))
so you can call them straight from wire:click or from $component.
Related
- Actions — the classes these modals belong to
- Modals — the modal classes themselves, used without an action
- Forms — the schema a form modal renders
- Infolists — what an infolist modal shows
- Lifecycle And Queues — what runs after the modal is submitted