Route modals
Render the modal in an @modal slot; close it with router.back().
From next-best-practices, parallel-routes.md.
Closing is a history operation
Open a photo, close it, then press Back.
router.push('/')BadHistory
Backs to leave 1
router.back()GoodHistory
Backs to leave 1
// components/modal.tsx
'use client'
import { useRouter } from 'next/navigation'
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter()
const close = () => router.back()
return (
<div
className="overlay"
onClick={(e) => e.target === e.currentTarget && close()}
onKeyDown={(e) => e.key === 'Escape' && close()}
>
<div role="dialog" aria-modal="true">
<button onClick={close}>Close</button>
{children}
</div>
</div>
)
}
Reload skips the intercept
Open a photo, press Refresh, then untick default.tsx.
BrowserHard load
What renderedapp/
- layout.tsx{ children, modal }
- page.tsxchildren
- photos/[id]/page.tsxchildren
- @modal/default.tsxmodal
- @modal/(.)photos/[id]/page.tsxmodal
// app/layout.tsx
export default function RootLayout({ children, modal }: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (<html><body>{children}{modal}</body></html>)
}
// app/@modal/default.tsx
export default function Default() {
return null
}
Matchers count segments
Pick a slot and a matcher.
Read as foldersWrong
You expect it to intercept
Read as route segmentsRight
It actually intercepts
Why it matters, pros and cons
Why it matters
Opening was a navigation, so closing must undo it. push reopens it on Back.
Pros
- Shareable URL; refresh shows the full page.
- Back and Forward work as expected.
- The gallery keeps its scroll underneath.
Cons
- Two pages per photo to keep in sync.
- Landing directly,
router.back()leaves your site. - A URL-less dialog needs none of this.