Modals
Modal::make($id)->title('…')->schema([...]) declares a dialog. It never sits in the page tree on
its own — a single host renders it, reached one of two ways.
Trigger-embedded
Section titled “Trigger-embedded”Pass a Modal to ->modal() on Button, Link, or MenuItem. Clicking the trigger opens it —
no id to wire up, no effect to dispatch:
use Lattice\Ui\Components\Button;use Lattice\Ui\Components\Modal;
Button::make('Invite') ->modal( Modal::make('invite-member') ->title('Invite a member') ->description('They will receive an email invitation.') ->schema([ // form fields, text, buttons… ]), );->modal() also accepts a closure returning a Modal, resolved like any other
closure parameter at serialization time — reach for it when the modal’s
content depends on data that is expensive to build up front, or should be re-evaluated per render:
Button::make('Details')->modal(fn (): Modal => Modal::make('order-details') ->title("Order #{$this->order->id}") ->schema([...]));A clickable component carries exactly one behavior: an href, an action, effects, or a modal.
Setting a second one throws — Button::make('Details')->href('/x')->modal(...) is rejected the same
way ->href()->action() is.
Effect-shipped
Section titled “Effect-shipped”An action can ship a modal instead of embedding one on a trigger — the modal
travels in the open-modal effect’s payload:
use Lattice\Actions\ActionResult;use Lattice\Ui\Components\Modal;
return ActionResult::success()->openModal( Modal::make('order-details') ->title('Order details') ->schema([...]),);This is the right shape when the modal’s content depends on server-side work the action just did — looking up a record, rendering a generated document — rather than data already available at the trigger.
For a modal that should be open on page load, flash the same effect from a controller or middleware
instead of embedding it in the tree, with Effects::flash():
use Lattice\Facades\Effects;use Lattice\Ui\Components\Modal;
Effects::flash(Effects::openModal( Modal::make('welcome')->title('Welcome back')->schema([...]),));
return redirect('/dashboard');->closeModal($id) closes the hosted modal if it matches $id; ->closeModal() with no argument
closes every modal currently open, not just the topmost one.
->title('…')and->description('…')set the dialog header.->closeLabel('…')relabels the close button (defaults toClose).->slideOut()docks the dialog to a viewport edge as a full-height sheet;->slideOut(Side::Start)picks the leading edge instead of the trailing one.->width(ModalWidth::…)sets the dialog width — the max width of a centered dialog and the panel width of a sheet. Defaults toModalWidth::Lg(32rem).
Slide-out sheets
Section titled “Slide-out sheets”->slideOut() presents the dialog as a full-height sheet docked to a viewport edge instead of a
centered window — the pattern for quick previews and edits alongside a table. The default edge is
the trailing one; pass a Side to dock it to the leading edge.
use Lattice\Ui\Enums\ModalWidth;use Lattice\Ui\Enums\Side;
Modal::make('order-preview') ->title('Order #1042') ->slideOut() ->width(ModalWidth::Xl2) ->schema([ // … ]);
Modal::make('saved-filters') ->title('Saved filters') ->slideOut(Side::Start) ->schema([ // … ]);Sheets and centered dialogs share the same width scale: ->width() caps a centered dialog and sets
a sheet’s panel width.
Stacking
Section titled “Stacking”Opening a modal while another is already open — a button’s ->modal() inside a modal’s schema, or
a row action’s confirmation dialog above a modal that contains its table — pushes a new entry onto
the host rather than replacing the current one. Closing the top entry (Escape or its close button)
reveals whatever was open underneath; ->closeModal() with no id closes the entire stack instead of
just the topmost entry.
The host itself is cleared on real page navigation — an open modal does not survive a redirect or a
link to another page, since its content could otherwise hold stale props from the page it was opened
on. A partial reload (->reloadComponent(), polling, an action’s own table refresh) does not count
as navigation and leaves an open modal untouched.
Lazy content
Section titled “Lazy content”A modal’s schema can include a Fragment::lazy(...) — its content fetches from the server the first
time the modal opens, not when the trigger renders, so a rarely-opened modal never pays for data the
page doesn’t need:
use Lattice\Fragments\Components\Fragment;
Button::make('Activity') ->modal( Modal::make('activity-log') ->title('Activity log') ->schema([ Fragment::lazy(ActivityLogFragment::class), ]), );See Fragments for how Fragment::lazy() works.