Intercepting routes allow you to load a route within the current layout while showing a different URL. This enables powerful patterns like modals that have shareable URLs, where clicking a link shows a modal overlay but navigating directly (or refreshing) shows the full page.
Common use cases include:
/login renders a full page@slot)Create parallel route slots using the @ prefix:
app/├── _layout.tsx # Receives { children, modal } props├── @modal/│ ├── default.tsx # Renders null when no intercept is active│ └── (.)photos/│ └── [id].tsx # Intercepts /photos/[id]├── photos/│ └── [id].tsx # Full page /photos/[id]└── feed.tsxUse parentheses with dots to define intercept relationships:
| Convention | Meaning | Example |
|---|---|---|
(.) | Same level | (.)photos intercepts sibling photos route |
(..) | One level up | (..)settings intercepts parent’s settings route |
(..)(..) | Two levels up | (..)(..)admin intercepts grandparent’s route |
(...) | From root | (...)photos intercepts /photos from anywhere |
Create an @modal directory (or any name) for your slot:
app/├── @modal/│ └── default.tsxThe default.tsx renders when no intercept is active:
export default function ModalDefault() { return null}Create the intercept route using the (.) prefix:
import { useParams, useRouter } from 'one'
export default function PhotoModal() { const { id } = useParams<{ id: string }>() const router = useRouter()
return ( <div className="modal-overlay" onClick={() => router.back()}> <div className="modal-content" onClick={(e) => e.stopPropagation()}> <h2>Photo {id}</h2> <img src={`/images/photo-${id}.jpg`} alt={`Photo ${id}`} /> <button onClick={() => router.back()}>Close</button> </div> </div> )}This renders on direct navigation or page refresh:
import { useParams, Link } from 'one'
export default function PhotoPage() { const { id } = useParams<{ id: string }>()
return ( <div className="photo-page"> <h1>Photo {id}</h1> <img src={`/images/photo-${id}.jpg`} alt={`Photo ${id}`} /> <Link href="/photos">Back to gallery</Link> </div> )}The layout receives slot props for each @slot directory:
import { Slot } from 'one'
export default function RootLayout({ children, modal, // From @modal directory}: { children: React.ReactNode modal: React.ReactNode}) { return ( <html> <body> {children} {modal} </body> </html> )}When a user clicks a link within your app:
/photos/5)When a user navigates directly to a URL or refreshes:
This ensures URLs are always shareable and work as expected.
Here’s a full photo gallery with modal implementation:
import { Slot } from 'one'
export default function RootLayout({ children, modal }) { return ( <html> <head> <meta charSet="utf-8" /> <title>Photo Gallery</title> </head> <body> {children} {modal} </body> </html> )}import { Link } from 'one'
const photos = [ { id: '1', title: 'Sunset' }, { id: '2', title: 'Mountains' }, { id: '3', title: 'Ocean' },]
export default function PhotoGrid() { return ( <div className="photo-grid"> <h1>Photo Gallery</h1> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}> {photos.map((photo) => ( <Link key={photo.id} href={`/photos/${photo.id}`}> <img src={`/thumbnails/${photo.id}.jpg`} alt={photo.title} /> <span>{photo.title}</span> </Link> ))} </div> </div> )}import { useParams, useRouter } from 'one'
export default function PhotoModal() { const { id } = useParams<{ id: string }>() const router = useRouter()
return ( <div className="modal-overlay" style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', }} onClick={() => router.back()} > <div className="modal-content" style={{ background: 'white', padding: 24, borderRadius: 8, maxWidth: '90vw', maxHeight: '90vh', }} onClick={(e) => e.stopPropagation()} > <img src={`/images/photo-${id}.jpg`} alt={`Photo ${id}`} style={{ maxWidth: '100%', maxHeight: '70vh' }} /> <div style={{ marginTop: 16, display: 'flex', gap: 8 }}> <button onClick={() => router.back()}>Close</button> <button onClick={() => router.push(`/photos/${id}`)}> View Full Page </button> </div> </div> </div> )}import { useParams, Link } from 'one'
export default function PhotoDetail() { const { id } = useParams<{ id: string }>()
return ( <div className="photo-detail"> <Link href="/photos">← Back to gallery</Link> <h1>Photo {id}</h1> <img src={`/images/photo-${id}.jpg`} alt={`Photo ${id}`} style={{ width: '100%', maxWidth: 800 }} /> <div className="photo-info"> <p>Full resolution image with metadata and comments...</p> </div> </div> )}export default function ModalDefault() { return null}You can have multiple slots for different purposes:
app/├── _layout.tsx # { children, modal, sidebar }├── @modal/│ └── ...├── @sidebar/│ ├── default.tsx│ └── (.)cart.tsx # Intercepts /cart as sidebar└── ...export default function RootLayout({ children, modal, sidebar }) { return ( <html> <body> <main>{children}</main> {modal} {sidebar} </body> </html> )}router.back() to close modals - this properly restores the previous URL and stateIf you were using the previous routeMasks configuration, migrate to intercepting routes:
Before (route masking):
one({ router: { routeMasks: [ { from: '/photos/[id]/modal', to: '/photos/[id]' } ] }})After (intercepting routes):
app/├── @modal/(.)photos/[id].tsx # Modal version├── @modal/default.tsx # Returns null└── photos/[id].tsx # Full pageThe intercepting routes approach is more powerful because: