Next.js Breadcrumb — Server-Rendered Route Trails
Add a Next.js Breadcrumb that renders on the server with zero hooks. Drivn auto-inserts separators and ships accessible ARIA markup in pure React and Tailwind.
A breadcrumb is the one navigation component that should never ship JavaScript. It is a static trail of links mirroring a URL the server already knows, it holds no state, and it never re-renders after load. Yet many React breadcrumb implementations pull in a client-side primitive or wire up context for something a <nav> and an <ol> express natively.
Drivn's Breadcrumb stays on the server. The component file contains no 'use client' directive, no hook, and no browser API — it walks its children with React.Children.toArray and inserts a separator before every item after the first, so you never write chevrons by hand. The markup it produces is the accessibility pattern screen readers expect: a <nav aria-label="Breadcrumb"> wrapping an ordered list, with aria-current="page" on the final item and every separator hidden behind aria-hidden.
This guide installs Drivn in a Next.js 16 project, renders a breadcrumb inside a server component, generates the trail dynamically from route params, swaps the separator and collapses deep paths with the ellipsis, and converts the links to next/link for client-side transitions. Every snippet matches the source the CLI installs. For the full API see the Breadcrumb docs; for the head-to-head see Drivn vs shadcn/ui Breadcrumb.
Install in a Next.js 16 project
Drivn installs through a CLI that writes component source into your repository — no runtime package, no version to track. From the root of your Next.js 16 project run npx drivn add breadcrumb. The CLI prompts once for an install directory, defaulting to src/components/ui/, then writes breadcrumb.tsx into place. The component imports two icons from lucide-react — ChevronRight for the default separator and MoreHorizontal for the ellipsis — so make sure that package is in your project; everything else is React, Tailwind, and the local cn class-merge helper. The CLI reference documents custom paths and multi-component installs, and the installation guide covers project prerequisites. Once the file lands it is yours — future Drivn releases never overwrite it.
1 # from the root of your Next.js 16 project 2 npx drivn add breadcrumb
Render inside a Server Component
App Router pages are server components by default, and the Breadcrumb renders inside one without any directive: the file has no 'use client', no state, and no effect, so Next.js ships the trail as static HTML with zero JavaScript attached. Compose it from sub-components — Breadcrumb.Item for each link and Breadcrumb.Page for the current page. You never insert separators yourself: the root maps over React.Children.toArray(children) and prepends a Breadcrumb.Separator before every child after the first, skipping children that already are separators. The rendered markup is the canonical accessible structure — <nav aria-label="Breadcrumb"> around an <ol>, the current page marked aria-current="page", and each separator wrapped in role="presentation" with aria-hidden="true" so screen readers announce the trail, not the chevrons.
1 // app/docs/page.tsx — server component 2 import { Breadcrumb } from '@/components/ui/breadcrumb' 3 4 export default function Page() { 5 return ( 6 <Breadcrumb> 7 <Breadcrumb.Item href="/">Home</Breadcrumb.Item> 8 <Breadcrumb.Item href="/docs">Docs</Breadcrumb.Item> 9 <Breadcrumb.Page>Button</Breadcrumb.Page> 10 </Breadcrumb> 11 ) 12 }
Generate the trail from route segments
Hand-written breadcrumbs drift the moment a route is renamed. In the App Router the URL segments are already available to every dynamic page, so the trail can be derived instead of maintained. Await params in a server component, map every segment except the last to a Breadcrumb.Item whose href is the cumulative path, and render the final segment as Breadcrumb.Page. Because React.Children.toArray flattens mapped arrays, the automatic separator insertion works exactly as it does with hand-written children — every mapped item gets its chevron with no extra markup. Format the raw segment however your routes demand (replace dashes, capitalize words); the Breadcrumb examples page shows a formatting helper in a finished layout.
1 // app/docs/[...slug]/page.tsx — server component 2 import { Breadcrumb } from '@/components/ui/breadcrumb' 3 4 export default async function DocsPage({ 5 params, 6 }: { 7 params: Promise<{ slug: string[] }> 8 }) { 9 const { slug } = await params 10 return ( 11 <Breadcrumb> 12 <Breadcrumb.Item href="/">Home</Breadcrumb.Item> 13 {slug.slice(0, -1).map((segment, i) => ( 14 <Breadcrumb.Item 15 key={segment} 16 href={`/docs/${slug.slice(0, i + 1).join('/')}`} 17 > 18 {segment} 19 </Breadcrumb.Item> 20 ))} 21 <Breadcrumb.Page>{slug[slug.length - 1]}</Breadcrumb.Page> 22 </Breadcrumb> 23 ) 24 }
Custom separators and deep paths
The default separator is a ChevronRight icon, sized by the [&>svg]:size-3.5 rule in the component's separator styles and dimmed to 60% muted-foreground. To use a different glyph, pass separator to the root — a slash, a dot, an arrow, any React node — and the auto-insertion renders it between every pair of items. For deep hierarchies, Breadcrumb.Ellipsis collapses the middle of the trail: it renders a MoreHorizontal icon with screen-reader-only text reading "More pages", so the markup stays accessible while the visible trail stays short. Keep the first and last items visible either way — users orient on where a path starts and ends, and the Breadcrumb docs list every sub-component this pattern composes from.
1 // custom separator 2 <Breadcrumb separator={<span>/</span>}> 3 <Breadcrumb.Item href="/">Home</Breadcrumb.Item> 4 <Breadcrumb.Item href="/settings">Settings</Breadcrumb.Item> 5 <Breadcrumb.Page>Profile</Breadcrumb.Page> 6 </Breadcrumb> 7 8 // collapse a deep path 9 <Breadcrumb> 10 <Breadcrumb.Item href="/">Home</Breadcrumb.Item> 11 <Breadcrumb.Ellipsis /> 12 <Breadcrumb.Item href="/docs/components"> 13 Components 14 </Breadcrumb.Item> 15 <Breadcrumb.Page>Button</Breadcrumb.Page> 16 </Breadcrumb>
Switch the links to next/link
Breadcrumb.Item renders a plain anchor — the source spreads its props onto an <a> element — which in Next.js triggers a full document navigation on click. For breadcrumbs that is often acceptable, but if you want the same prefetching and soft transitions as the rest of your app, the copy-and-own model makes the upgrade a one-line edit: import Link from next/link inside breadcrumb.tsx and swap the <a> for it. Nothing else changes — Link accepts the same href, className, and anchor attributes the component already passes through, and the React.AnchorHTMLAttributes prop type keeps every call site type-checked. Because the file lives in your repo, the edit is permanent; no library release can revert it.
1 // src/components/ui/breadcrumb.tsx — swap <a> for next/link 2 import Link from 'next/link' 3 4 function Item({ 5 href, 6 className, 7 children, 8 ...props 9 }: React.AnchorHTMLAttributes<HTMLAnchorElement> & { 10 href: string 11 children: React.ReactNode 12 }) { 13 return ( 14 <li> 15 <Link 16 href={href} 17 className={cn(styles.link, className)} 18 {...props} 19 > 20 {children} 21 </Link> 22 </li> 23 ) 24 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
No. The component holds no state, runs no effect, and touches no browser API — the separator insertion happens during render via React.Children.toArray, which works identically on the server. There is no 'use client' directive anywhere in the file, so App Router pages render the whole trail as static HTML with zero JavaScript shipped for it.
Pass any React node as the separator prop on the root — a slash in a span, a dot, an SVG — and the automatic insertion renders it between every pair of items. The default is a ChevronRight icon, applied as a fallback inside the Separator sub-component, so changing the default for the whole app is a one-line edit to that fallback in your copy of breadcrumb.tsx.
Not by itself — it renders exactly the items you compose, which keeps the component free of router coupling. In the App Router, derive the trail in a server component: await params, map the URL segments to Breadcrumb.Item elements with cumulative href paths, and render the last segment as Breadcrumb.Page. Separators are still inserted automatically for mapped arrays.
Yes. The root renders a <nav aria-label="Breadcrumb"> wrapping an <ol>, the current page is a <span> inside a list item marked aria-current="page", separators carry role="presentation" and aria-hidden="true" so they are skipped by screen readers, and the ellipsis includes visually hidden "More pages" text. That is the WAI-ARIA breadcrumb pattern, shipped as-is.

