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

Nova Modal Response Laravel Package

markwalet/nova-modal-response

A Laravel Nova package that lets actions return modal responses. Show custom content, confirmations or messages in a modal without leaving the page—just return a ModalResponse from your action's handle method.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the surface. It's a Composer-only dependency — no migrations, no config file, no service provider to register. Publish/rebuild the front-end assets as part of the same deploy (see Gotchas), then verify a Nova action can return a modal.

composer require markwalet/nova-modal-response

Where to look first, in this order:

  1. Markwalet\NovaModalResponse\ModalResponse — the entry point; it is a Nova ActionResponse, so it is returned directly from handle().
  2. Markwalet\NovaModalResponse\Block — the block factory. This is the real API surface in v2; skim its methods once and you have seen the whole feature set (text, heading, list, divider, code, json, html, markdown, view, badge, icon, link, inline, collapsible, tabs).
  3. docs/adr/0001-single-render-path-with-stacked-blocks.md if you are coming from v1 — it explains why the payload changed.
  4. UPGRADE.md — only if you already ship modals from a v1 install.

Your first use case should be the one that pays for itself immediately: a post-action diagnostic. Something a user must read, that today forces a redirect to a custom page or gets buried in a log.

use Markwalet\NovaModalResponse\ModalResponse;

public function handle(ActionFields $fields, Collection $models): ModalResponse
{
    [$rows, $warnings] = $this->runImport($models);

    return ModalResponse::stack([
        Block::heading('Import finished')->large(),
        Block::text("Imported {$rows} rows."),
        Block::list($warnings),
    ]);
}

If that renders, the rest of the package is composition. Start with text, heading, list, then add code/json the first time you need to show something copyable — the built-in highlighting and clipboard button is the most concrete win over a plain toast or a custom Blade page.

Pick the right response type before you pick a block. The package is for read/inspect outcomes only:

Outcome Return
One-line confirmation Action::message(...)
Something to read, inspect, copy ModalResponse::*
A file Action::download(...)
Navigation Action::redirect(...) / Action::visit(...)

Implementation Patterns

Single block vs. stack — decide by content shape, not by habit. One piece of content gets a shortcut factory; several pieces get stack(). Do not over-structure a two-line confirmation into a five-block stack.

ModalResponse::text('The export finished successfully.');
ModalResponse::code($generatedScript);              // highlighted, with copy button
ModalResponse::code($log, highlight: false);        // plain text
ModalResponse::json($model->toArray());             // array in, pretty JSON out
ModalResponse::markdown("## Done\n\nImported **42** rows.");
ModalResponse::view('reports.summary', ['report' => $report]);
ModalResponse::tabs(['Summary' => [...], 'Log' => [...]]);

Use stack() with a closure when blocks are expensive or conditional. stack() accepts an array or a closure returning one, which lets you defer work and skip blocks that aren't needed.

return ModalResponse::stack(fn () => [
    Block::heading('Sync report')->large(),
    Block::inline([
        'Status:',
        Block::badge($result->failed ? 'Failed' : 'OK')
            ->variant($result->failed ? 'danger' : 'success')
            ->icon('check-circle'),
    ]),
    Block::divider(),
    ...$result->hasErrors() ? [Block::list($result->errors)] : [Block::text('No errors.')],
    Block::collapsible('Raw payload', [Block::json($result->payload)]),
]);

Leverage nested blocks for progressive disclosure. collapsible and tabs contain blocks, including code/json — and the copy button works at any nesting depth. This is how you keep a modal scannable while still offering the full log or payload to whoever wants it.

Tabs are your documentation pattern. Release notes, runbook links, and troubleshooting steps map cleanly onto one modal with a few tabs. Composing these from markdown + view blocks is far cheaper than commissioning a custom Nova page.

view blocks: repo-controlled Blade only. They render server-side with your app's Blade environment, which makes them the right tool for report fragments you already trust. It also makes them the wrong tool for anything request-derived.

Build a house style and encode it. A small private helper (App\Support\AdminModal) that returns a consistent title, size, and status badge means every action's modal looks the same without copy-pasting Block::badge(...)->success() everywhere. This is the difference between "we use this package" and "our admin panel has one modal language."

Type-hint the return as ModalResponse when the action always shows a modal. Nova accepts any ActionResponse, so widening to ActionResponse is fine when the branch is real (e.g. download on huge output, message on nothing-to-report), but the narrow type documents intent.

Keep actions thin. Do the domain work in a service/action class, then map the result to blocks in the Nova action. The block stack is a serialization of a result object, not a place to put business logic.

Gotchas and Tips

The v1→v2 break fails silently. The payload is now { title, size, closeButtonText, blocks: [...] }. Legacy v1 keys (body, code, html, highlight) produce a console.warn and a degraded — often empty — modal. No PHP exception. If you construct payloads directly via Action::modal('nova-modal-response', [...]), grep for it before upgrading:

grep -rn "modal('nova-modal-response'" app/
grep -rn "withoutSyntaxHighlighting" app/

withoutSyntaxHighlighting() is gone. Replace with ModalResponse::code($x, highlight: false) or ->withoutHighlighting() on a code/json block. This is a fatal error, so it will surface loudly — the payload change is the dangerous one.

Stale compiled assets are the most likely real-world failure. Bumping Composer without re-publishing/rebuilding the front-end leaves a v1 renderer consuming a v2 payload. Ship code and assets as one atomic unit, and only if you have published the package's views/assets — the default is bundled assets, which are why the upgrade is usually just a dependency bump.

Add one golden-payload test. Assert the shape of ModalResponse::stack([...])->jsonSerialize() (title, blocks array, block types). It is the only thing that will catch a future v3 breaking you, since the renderer degrades quietly. Cheap insurance.

Manual QA in both themes. Open every modal-returning action in light and dark mode, including at least one tabbed modal and one nested collapsible. Badges, icons, and code highlighting are the usual suspects; empty tabs and collapsed collapsibles hide payload errors.

Never feed untrusted input to markdown, html, or view. Markdown preserves javascript: links and most raw HTML; html is unsanitized by design. For user-controlled or AI-generated content use text, list, or code. Make this an explicit team convention, not a code-review catch.

Bare strings are text blocks. In a stack, inline group, collapsible, or tab, a bare string becomes a Block::text(). Convenient, but it also means a mistakenly-stringified object in a block array silently renders as text instead of erroring — check your json blocks take an array, not an encoded string.

Remember the copy button is the point. Code and JSON blocks include copy-to-clipboard at every nesting level. If you find yourself rendering a payload in a text block, you've lost the single best justification for adopting the package.

Prefer the enums to string literals. Size (small/medium/large) and Variant (default/info/success/warning/danger) are shared across heading, badge, icon, and link blocks. Using them keeps modals visually coherent and makes a future restyle a one-file change.

Ordering matters for upgrade hygiene. If Laravel Boost is in use, upgrade Boost to ≥1.3.0 before the package. Record v1.3.0 of this package as the rollback target.

Treat it as a convenience dependency, not infrastructure. Small star count, single maintainer, zero dependents. That is acceptable for internal admin UI precisely because the fallback — fork, vendor, or a handful of Blade views — is a day of work. Do not let it become load-bearing for customer-facing surfaces, and don't build interactive forms inside a modal: multi-step wizards and in-modal mutations are where this package stops and custom Vue begins.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect