Catch-All Segments

Catch-All and Optional Catch-All Segments

An ellipsis inside the brackets extends a dynamic segment over the rest of the path. [...slug] matches one or more segments and hands you an array; [[...slug]] does the same but also matches the bare parent route, where the parameter is simply absent.

app/shop/[...slug]/page.jsShell
export default async function Shop({ params }) {
  const { slug } = await params
  return <pre>{JSON.stringify({ slug })}</pre>
}

Both demo routes print their own params, so the difference is visible directly in the served HTML:

Output of 10
/shop/clothes/tops/t-shirts   <pre>{"slug":["clothes","tops","t-shirts"]}</pre>
/docs/app/routing             <pre>{"slug":["app","routing"]}</pre>
/docs                         <pre>{}</pre>

/docs renders with no parameter at all, while /shop — the same route without the extra brackets — is one of the 404s in the probe of Route Groups.

What each bracket form matches and what params holds
Route /shop /shop/a /shop/a/b
[slug] 404 'a' 404
[...slug] 404 ['a'] ['a', 'b']
[[...slug]] undefined ['a'] ['a', 'b']

Use the optional form whenever one component should own a whole subtree including its index — documentation sites, file browsers, CMS-driven pages. The parameter may be undefined there, so slug?.join('/') ?? '' is the normal way to rebuild the path. A catch-all also swallows everything below it, including URLs you meant to 404; validate the array and call notFound() rather than rendering an empty shell.