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

Modal Laravel Package

inertiaui/modal

inertiaui/modal brings powerful modal windows to Laravel Inertia apps. Create route-driven modals, nested modals, slideovers, and persistent layouts with minimal setup—no separate API required. Works with Vue 3, React, and Svelte through one simple, elegant API.

View on GitHub
Deep Wiki
Context7

Getting Started

Install both halves — they must match.

composer require inertiaui/modal
npm install @inertiaui/modal-vue   # or @inertiaui/modal-react

The PHP package registers an Inertia::modal() response macro via its service provider; the JS package provides <ModalLink>, <ModalRoot>, and the useModal() composable. The PHP and JS packages are versioned together — pin them to the same major version or you will get silent no-ops (Inertia::modal() returns a response the frontend doesn't understand, or vice versa).

Wire up the root once. In your app shell/layout (the component that wraps every page, not an individual page), render the modal root:

<template>
  <ModalRoot />
  <slot />
</template>

Forgetting this is the single most common "nothing happens" failure.

First use case — link to an existing route, change nothing else. Open your users.index page and swap a plain Link for a ModalLink:

<ModalLink href="/users/create" label="New user" />

You did not touch routes/web.php. UsersController@create still returns Inertia::render('Users/Create'). The package intercepts the visit, renders the page inside a modal, and keeps the URL/page underneath intact. That's the whole value proposition: modals for free on routes you already have.

Where to look first:

  1. https://inertiaui.com/modal — the docs are genuinely good and the versioned upgrade guides matter here.
  2. config/inertia-modal.php after php artisan vendor:publish --tag=inertia-modal — browser history behaviour, default modal width, local modals.
  3. vendor/inertiaui/modal/src — it's a small package; reading ModalResponse.php answers most "why did it do that" questions faster than the docs.

Implementation Patterns

Link-driven modals (the 80% case). ModalLink renders an <a> by default, so it's accessible and middle-clickable. Use as="button" for row actions, method="post" + data for destructive actions:

<ModalLink
  :href="`/users/${user.id}`"
  method="delete"
  as="button"
  class="text-red-600"
  @success="() => table.reload()"
>Delete</ModalLink>

Server-driven modals when you need a shareable URL. Any page opened with ModalLink can also be visited directly — but only if the server knows what to render behind the modal. That's what baseUrl is for:

// UsersController@edit
return Inertia::modal('Users/Edit', ['user' => $user])
    ->baseUrl(route('users.index'));

Now /users/42/edit opens the index page with the edit modal on top when hit as a full page load. Without baseUrl, a direct visit renders the edit form as a bare page. Do this for anything users will bookmark, refresh, or share.

Emitting results from modal to parent. Don't reach into parent state with a global store. Use the modal event bus:

<!-- inside Users/Create.vue -->
<script setup>
import { useModal } from '@inertiaui/modal-vue'
const modal = useModal()

function submit() {
  form.post('/users', {
    onSuccess: () => {
      modal.emit('created', form.data())
      modal.close()
    },
  })
}
</script>
<ModalLink href="/users/create" @created="(user) => users.unshift(user)" />

useModal() is the imperative escape hatch. From inside modal content you get modal.props (the page props), modal.close(), modal.reload(), modal.setOpen(false), and modal.afterLeave(cb) — the last one is how you defer focus restoration or scrolling until the leave transition finishes. Use afterLeave rather than nextTick when touching the DOM behind the modal.

Nested modals. A ModalLink rendered inside modal content opens a second modal stacked on the first, and closing the child returns to the parent with its state intact. This is the cleanest way to do "edit → confirm delete" flows. Combine with closeExplicitly (blocks overlay/escape close) and closeButton={false} for anything with form data at risk.

Slideover for detail views, modal for forms. Same route, different presentation: pass slideover on the ModalLink. Reserve it for read-heavy panels (activity logs, order details); forms in a full-width slideover feel awkward on mobile.

Confirm dialogs and self-contained modals. For UI that isn't a route at all (confirm delete, quick filters), the package supports local modals declared in the page/layout with the <Modal name="..."> component, opened via the useModal() state. This keeps trivial dialogs out of your routes file entirely.

Composition with existing table tooling. Row actions typically become a ModalLink inside your dropdown component. Keep the ModalLink as the outermost interactive element and let the dropdown close on click — a dropdown that stays open behind the modal is a z-index/teardown problem, not a package bug.

Testing. Modal routes are ordinary Inertia routes. $this->get(route('users.edit', $user))->assertInertia(fn ($p) => $p->component('Users/Edit')->has('user')) works unchanged. Test baseUrl behaviour with a non-Inertia (plain browser) request; the package returns a different payload for X-Inertia requests vs. direct hits, and that branch is where bugs hide.

Gotchas and Tips

Version skew is the #1 support ticket. Composer inertiaui/modal and npm @inertiaui/modal-vue must be the same major. v1 → v2 changed useModal() return shape and introduced local modals. If a ModalLink renders a normal link and navigates away, check versions before anything else.

The modal route must work standalone. Because a direct visit renders it inside the base page, Users/Edit.vue cannot assume it was mounted by the index page (no parent-provided props, no shared composable state). Pass everything you need as page props from the controller. Any component that only works inside a modal is a latent bug.

Layout duplication. Modal content inherits the layout of the page it's on, not its own. If Users/Edit has its own layout declaration, you'll get double-wrapped output. Keep modal pages layout-free and let the base page own the chrome.

Browser history and the back button. With history enabled, opening a modal pushes an entry and closing pops it. If your app also does router.visit() on modal close, you'll double-navigate and land a page too far back. Pick one: let modal.close() handle history, or disable browser history in config for that flow. Symptom is "back button goes forward".

Returning redirect() from a modal submit tears down the modal and navigates the whole page — which is often what you want, but it bypasses afterLeave and your parent never hears the emit. If you need the parent to update, return back() or nothing, and close client-side with modal.emit() first.

Events are fire-and-forget. Emit payloads are not persisted; a modal.reload() or a refresh loses them. For anything durable, put it in props and call router.reload({ only: [...] }) on the base page instead. Also note the emitted payload is whatever you pass — don't assume it's the server's version of the resource (validation, casts, appended attributes will differ).

z-index and scroll lock. The package locks body scroll while open. If you already use Tailwind UI dialogs, Headless UI transitions, or a third-party Select2/datepicker, popovers can render under the modal panel. Fix by appending those popovers to the modal panel element or bumping their stacking context — not by raising the modal's z-index, which breaks nesting.

Styling knobs. Rather than overriding the panel with CSS, use the props: maxWidth, paddingClasses, panelClasses. Overriding internals with deep selectors breaks on minor upgrades; the props are the supported extension point. Set the app-wide default width in config/inertia-modal.php.

Deep links lose query state unless you ask for it. If the base page has filters (?status=active), pass ->withQueryString() on the modal response or use ModalLink's preserve-scroll/query options — otherwise closing the modal rewrites the URL back to the unfiltered base.

Cache clearing after composer updates. Inertia::modal() is a macro registered by the service provider. "Method does not exist" after an update almost always means php artisan config:clear / optimize:clear against a stale cached provider manifest.

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.
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
youbar/easy-crud
c-point-labs/laravel-hostyourai
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