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

Inertia Modal Laravel Package

emargareten/inertia-modal

Inertia Modal lets Laravel and Inertia.js apps render pages as modals without extra routes or controllers, keeping server-side routing intact and navigation fast.

View on GitHub
Deep Wiki
Context7

Getting Started

The package solves a specific Inertia pain point: a controller needs to return a full page for a direct visit, but the same route should open as a modal when navigated to from within the app. Instead of duplicating routes or hand-rolling modal state in a global store, you return a modal-aware Inertia response from the backend and let the package's frontend outlet render it.

Install and wire up

  1. Install the Composer package and the matching frontend package (the JS side is required — the PHP package only produces the payload).
  2. Publish the config if you need to change the modal root element, component namespace, or middleware behavior:
    php artisan vendor:publish --tag="inertia-modal-config"
    
  3. Register the modal outlet once, in your persistent Inertia layout (not inside individual pages):
    <!-- resources/js/Layouts/AppLayout.vue -->
    <template>
      <slot />
      <ModalRoot /> <!-- name per README; often <InertiaModal /> or <Modal /> -->
    </template>
    
  4. Confirm the middleware is active. If it is not auto-discovered, add it to the web group in bootstrap/app.php (Laravel 11+) or Kernel.php.

Where to look first

  • The README's "Usage" example — it is short and shows the full request/response cycle.
  • config/inertia-modal.php — the root component name, modal component resolver, and any URL/base-route settings live here.
  • The package's frontend component source — it is the authoritative answer for prop names, events, and slot behavior.
  • Your app/Http/Middleware registration and HandleInertiaRequests — most "modal doesn't open" bugs are middleware-ordering or shared-prop issues.

First use case: edit-from-index

The canonical flow is an index page with an "Edit" link that opens a form modal without losing the index page's scroll position, filters, or pagination:

// UserController
public function index()
{
    return inertia('Users/Index', [
        'users' => User::paginate(),
    ]);
}

public function edit(User $user)
{
    return Modal::render('Users/Edit', [
        'user' => $user,
    ]);
}
<Link :href="`/users/${user.id}/edit`">Edit</Link>

On a normal visit /users/1/edit renders a full page; when clicked from the index, the outlet intercepts the response and displays the modal over the existing page. That single behavior is the package's entire value proposition — everything else is convention around it.

Implementation Patterns

Keep index and modal routes separate, but let one controller method serve both. You generally want /users for the page and /users/{user}/edit for the modal. Deep-linking and refreshing the modal URL should still produce something usable — either the modal over a fallback base page, or a full-page edit form. Check the package's base-route/fallback option and use it; a modal route that 500s on refresh is the most common production complaint.

Form submissions inside modals. Use a normal Inertia useForm inside the modal component. The key detail is what happens on success: redirect()->route('users.index') from a modal context can replace the background page or close everything, depending on the package version. Prefer a redirect back to the underlying page URL and let the modal close, or emit the package's close event after a successful post. Preserve the background page's state with preserveState/preserveScroll where applicable.

public function update(UpdateUserRequest $request, User $user)
{
    $user->update($request->validated());

    return redirect()
        ->route('users.index')
        ->with('success', 'User updated.');
}

Validation errors. Because the modal is a real Inertia visit, validation errors arrive in the normal errors prop. Do not render a second error bag or re-fetch the modal. If errors are not appearing, the usual cause is a redirect that changes the component rather than staying on the modal component (back() vs. an explicit route).

Stacked and nested modals. Treat modal depth as UI state, not backend state. If the package supports stacking, keep the stack shallow (two levels maximum); each layer needs its own focus trap and ESC handling, and the browser back button behavior becomes unintuitive quickly. For wizard-style flows, prefer a single modal with internal steps over multiple stacked modals.

Reusable modal components. Give modal components the same prop contract as their full-page counterparts. A common pattern is one UserForm component rendered by both Users/Edit (page) and Users/EditModal (modal shell), so validation and field logic are not duplicated.

Shared props and authorization. Modal routes are ordinary routes: they must carry the same auth, verified, and policy middleware as their page counterparts. A modal is not a security boundary. Also remember that Inertia shared props (HandleInertiaRequests::share) are evaluated for modal requests too — keep that method cheap, or modals will pay the cost on every open.

Lazy and closure props. Props are serialized per request. If a modal only needs a small slice of a model, return that slice explicitly rather than passing the whole model plus relationships; the modal request is a separate round trip and you will notice the payload size.

Testing. Assert against the Inertia response rather than the rendered DOM. In feature tests, use AssertableInertia to confirm the component and modal flag/props, and use assertRedirect for the post-update flow. For the frontend behavior, one or two browser tests (Dusk/Playwright) covering "click edit → modal opens → submit → modal closes" is usually enough; don't try to unit-test the outlet component.

Integration with resource controllers. Keep edit returning the modal, create returning the modal for new records, and store/update returning redirects. This maps cleanly onto Route::resource and means the index page never needs to know which actions are modalized.

Gotchas and Tips

  • Direct visits and refreshes. A modal route hit directly (new tab, refresh, crawler) has no background page. Configure a base/fallback URL so the package can render the modal over a sensible page, or accept that the route renders full-page. Test this explicitly — it is the number-one source of "works locally, breaks in production" reports.
  • The outlet must live in the persistent layout. Placing <ModalRoot /> inside a page component means it unmounts on navigation and modals silently stop appearing. Put it in the layout that wraps every page.
  • Preserve state vs. reset. preserveState: false (the default for many Inertia visits) will remount the background page and wipe local component state. When opening a modal from a filtered/paginated index, verify the background state survives and set preserveState/preserveScroll deliberately. If your filters live in local refs rather than the URL, they will not survive a remount — move them to query params.
  • Browser back button. Opening a modal is a history push. Closing it with a UI button should usually call router.visit(baseUrl) or history.back() consistently; mixing both produces double-history entries. Pick one close strategy and use it everywhere.
  • ESC key and click-outside. Do not attach a global keydown listener in each modal; nested or stacked modals will close multiple layers at once. Let the package handle dismissal, and only add custom handlers for unsaved-changes confirmation.
  • Focus management and accessibility. Verify focus moves into the modal and returns to the trigger on close, and that background content is aria-hidden or inert. If the package's outlet does not do this, wrap the modal content in a headless dialog primitive (Headless UI, Radix Vue, Reka UI) rather than reinventing it.
  • Debugging the payload. When a modal does not open, inspect the Inertia response in the network tab: the package communicates through a specific response key/header. If that key is missing, the middleware is not running or the controller is not using the modal response helper. If the key is present but nothing renders, the outlet is missing or the component name does not resolve — check the config's component namespace and your Vite aliases.
  • Middleware order. The package's middleware must run after session/auth middleware (it needs the authenticated user and shared props) and before the response leaves the web group. Registering it globally can break on API routes.
  • Component resolution is case-sensitive on Linux. A modal that resolves as Users/EditModal on macOS may fail in production if the file is Users/EditModal.vue vs. a directory mismatch. Keep modal component paths identical in casing to the filesystem.
  • Config quirks on upgrade. The published config is merged with the package default; if you published it early, new keys added in later releases may not exist in your copy. Re-publish to a temp location and diff after upgrades. This package is pre-1.0/early-stage (small star count, recent releases), so pin the version and read the changelog before composer update.
  • Extension points worth knowing. The component resolver (how a modal name maps to a Vue/React component) and the modal root element are the two seams you will actually customize — for example, rendering modals into a portal target above your toast container so toasts remain visible. Publish the frontend component if you need to change animation or stacking behavior; do not fork the PHP side for presentation concerns.
  • Keep modals thin. The failure mode of backend-driven modals is stuffing page-level data fetching into the modal response. If a modal needs three lazy-loaded relationship lists, it will feel slower than a page navigation. Return the minimum props, and fetch secondary data inside the modal only if the interaction genuinely requires it.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin