useSelectedLayoutSegments()
Read every active route segment below the layout the hook is called from.
import { useSelectedLayoutSegments } from 'next/navigation'
const segments = useSelectedLayoutSegments(parallelRouteKey?: string)- Added in Next.js v13.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$parallelRouteKeyoptional | string | Name of a parallel route slot to read the active segments from, without the @ prefix. |
Return value
string[]An array of the active segments below the calling layout, or an empty array when none exist. Route groups are included; catch-all matches appear as one joined string, so /blog/a/b/c read from app/layout.js gives ['blog', 'a/b/c'].
Errors
Calling it in a Server Component errors; render it from a Client Component imported into the layout.
Changelog
- PHP v13.0.0useSelectedLayoutSegments introduced.
useSelectedLayoutSegments returns the whole trail of active segments beneath the layout that renders it, which makes breadcrumbs almost free: the array is already in route order, and each prefix of it is a valid href. Read from the root layout on /dashboard/settings you get ['dashboard', 'settings']; read from app/dashboard/layout.js on the same URL you get ['settings']. Where the hook is called determines what you see.
What trips people up
- Route groups are included. A folder named
(marketing)shows up as the literal string'(marketing)'in the array, which then appears in your breadcrumb UI and in any href you build from it. Filter out anything starting with a bracket. - Catch-all routes are not split.
app/blog/[...slug]/page.json/blog/a/b/cyields['blog', 'a/b/c'], not four entries. Split on/if you want one crumb per level. - The values are raw segment names, so
[id]routes give you the id, not a title. Look up display names from your own data. - Layouts are Server Components, so the hook needs a
'use client'child; withcacheComponentsenabled it also suspends on dynamic params unknown at build time and needs aSuspenseboundary. - Parallel route slots are read by passing the slot name, without the
@.
Example
'use client'
import Link from 'next/link'
import { useSelectedLayoutSegments } from 'next/navigation'
export default function Breadcrumbs() {
const segments = useSelectedLayoutSegments()
// Route groups such as (marketing) are returned too — drop them
const crumbs = segments.filter((segment) => !segment.startsWith('('))
return (
<nav aria-label="Breadcrumb">
<ol>
<li>
<Link href="/">Home</Link>
</li>
{crumbs.map((segment, index) => {
const href = `/${crumbs.slice(0, index + 1).join('/')}`
const isLast = index === crumbs.length - 1
return (
<li key={href}>
{isLast ? (
<span aria-current="page">{segment}</span>
) : (
<Link href={href}>{segment}</Link>
)}
</li>
)
})}
</ol>
</nav>
)
}When to use it
- Render a breadcrumb trail from the root layout on every page.
- Show the current section depth in a documentation sidebar.
- Derive an analytics section label such as dashboard/settings.
- Decide whether a layout is showing a nested route or its own index page.
Frequently asked questions
How do I build breadcrumbs in the Next.js App Router?
Call useSelectedLayoutSegments in a Client Component rendered by your root layout, filter out route-group segments, and build each href by joining the array up to that index.
Why do my breadcrumbs show (marketing) or another folder in parentheses?
Route groups are included in the returned array. Filter with segments.filter((s) => !s.startsWith('(')) before rendering or before building hrefs from the values.
What is the difference between useSelectedLayoutSegments and useSelectedLayoutSegment?
The plural hook returns every active segment below the layout as an array; the singular one returns only the segment immediately below it, as a string or null.
More examples
What the hook returns, by call site
The same hook returns different arrays depending on which layout renders it: always the segments below that layout, never the layout's own path. Note the catch-all rows — the matched sub-path arrives as a single joined string rather than one entry per level.
// Called from app/layout.tsx
// / -> []
// /dashboard -> ['dashboard']
// /dashboard/settings -> ['dashboard', 'settings']
// /blog/a/b/c -> ['blog', 'a/b/c'] (catch-all joined)
// Called from app/dashboard/layout.tsx
// /dashboard -> []
// /dashboard/settings -> ['settings']
// Called from app/blog/layout.tsx
// /blog/a/b/c -> ['a/b/c']One crumb per level under a catch-all route
flatMap over a split turns the joined catch-all string into one entry per URL level, which is what a breadcrumb needs. Doing this unconditionally is safe: a normal segment contains no slash and passes through unchanged.
'use client'
import { useSelectedLayoutSegments } from 'next/navigation'
// app/docs/[...slug]/page.tsx, hook called from app/docs/layout.tsx
export function DocsTrail() {
const segments = useSelectedLayoutSegments()
// /docs/app/api-reference/functions -> ['app/api-reference/functions']
const parts = segments.flatMap((segment) => segment.split('/'))
// -> ['app', 'api-reference', 'functions']
return <p>{parts.join(' › ')}</p>
}Reading a parallel route slot
Passing the slot name (without the @) reads that parallel route instead of the default children slot, so a layout can tell whether an intercepted modal route is currently rendered and react to it — here by locking background scroll.
'use client'
import { useSelectedLayoutSegments } from 'next/navigation'
export function ModalStatus() {
// app/layout.tsx renders a @modal slot
const modalSegments = useSelectedLayoutSegments('modal')
// No modal route matched -> []
const isModalOpen = modalSegments.length > 0
return isModalOpen ? <div data-scroll-lock /> : null
}
Discussion