Modals
@stratal/inertia-modal lets a single Inertia route render a modal over another page. The address bar shows the modal URL; the user can refresh, share, or hit back without breaking the experience. There is no client-side modal state to wire up — the server decides what’s a modal and what isn’t.
When the user opens a modal, Stratal fetches the background page in-process, embeds the modal component and props in the response, and the client-side <Modal /> component renders the overlay. Closing the modal navigates back to the background URL.
Install
Section titled “Install”yarn add @stratal/inertia-modalServer setup
Section titled “Server setup”Add ModalModule to your application’s imports. It registers the modal service and adds an inertiaModal() method to RouterContext.
import { Module } from 'stratal/module'import { InertiaModule } from '@stratal/inertia'import { ModalModule } from '@stratal/inertia-modal'
@Module({ imports: [ InertiaModule.forRoot({ /* ... */ }), ModalModule, ],})export class AppModule {}Defining a modal route
Section titled “Defining a modal route”A modal route is a regular Inertia controller method. Call ctx.inertiaModal() instead of ctx.inertia() and tell it which background page should sit behind the modal:
import { Controller, Get, type RouterContext } from 'stratal/router'
@Controller('/notes')export class NotesController { @Get('/') index(ctx: RouterContext) { return ctx.inertia('notes/Index', { notes: [/* ... */] }) }
@Get('/:id/edit') async edit(ctx: RouterContext) { const note = await this.notes.findById(ctx.param('id')) return ctx.inertiaModal( 'notes/EditModal', { note }, { baseURL: '/notes' }, ) }}| Argument | Description |
|---|---|
component | Page component name to render inside the modal (resolved by your Inertia resolver, e.g. 'notes/EditModal'). |
props | Props passed to the modal component. |
options.baseURL | Path of the background page to render behind the modal. Used as the redirect target when the modal is closed. |
The modal URL (/notes/123/edit in the example) becomes the canonical URL. Bookmarking it, refreshing, or visiting it directly all show the modal over the background page.
Client setup
Section titled “Client setup”@stratal/inertia-modal ships its own page resolver so the client can dynamically load the modal component when one is active. Configure it in your Inertia entry file before createInertiaApp, then mount <Modal /> once in your root layout.
import { createInertiaApp } from '@inertiajs/react'import { resolver } from '@stratal/inertia-modal/react'
const pages = import.meta.glob('./pages/**/*.tsx')
resolver.set(async (name) => { const page = await pages[`./pages/${name}.tsx`]?.() if (!page) throw new Error(`Page not found: ${name}`) return page})
createInertiaApp({ resolve: async (name) => { const page = await pages[`./pages/${name}.tsx`]?.() if (!page) throw new Error(`Page not found: ${name}`) return page }, // ...})import { Modal } from '@stratal/inertia-modal/react'
export function DashboardLayout({ children }: { children: React.ReactNode }) { return ( <> <Sidebar /> <main>{children}</main> <Modal /> </> )}<Modal /> reads props.modal from the current Inertia page and renders the corresponding component. When no modal is active it renders nothing.
useModal()
Section titled “useModal()”Inside a modal page component, useModal() exposes the modal state and a helper to dismiss it.
import { useModal } from '@stratal/inertia-modal/react'import { router } from '@inertiajs/react'
interface Note { id: string title: string content: string}
export default function EditModal() { const { show, redirect, props } = useModal() const note = props?.note as Note | undefined
if (!show || !note) return null
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { event.preventDefault() const data = new FormData(event.currentTarget) router.post(`/notes/${note.id}`, data) }
return ( <dialog open onClose={redirect}> <h2>Edit note</h2> <form onSubmit={handleSubmit}> <input name="title" defaultValue={note.title} required /> <textarea name="content" defaultValue={note.content} /> <button type="submit">Save</button> <button type="button" onClick={redirect}>Cancel</button> </form> </dialog> )}| Field | Type | Description |
|---|---|---|
show | boolean | true when a modal is currently active on this page. |
props | Record<string, unknown> | undefined | The props passed from ctx.inertiaModal(). |
redirect() | () => void | Navigate back to the page that opened the modal (or baseURL on direct visits). |
Handling form submissions
Section titled “Handling form submissions”A modal form usually posts to a normal (non-modal) route and lets the response handle the redirect. Returning ctx.redirect('/notes') from your update handler closes the modal and returns the user to the background page.
@Post('/:id')async update(ctx: RouterContext) { const id = ctx.param('id') await this.notes.update(id, await ctx.request.json()) return ctx.redirect('/notes')}Errors
Section titled “Errors”| Error | HTTP status | When |
|---|---|---|
ModalBackgroundFetchError | 502 | The background page configured in baseURL could not be fetched (e.g. the route returned a 4xx/5xx). |
Make sure the page at baseURL is reachable for the same authenticated user that hit the modal route — modals run an internal sub-request that forwards the original cookies and host header.