How to Add a Breadcrumb to a React App
Add a copy-and-own Breadcrumb to any React app with the Drivn CLI — auto-inserted separators, accessible ARIA markup, and no runtime UI dependencies.
Every app accumulates depth. The settings page grows a billing section, the billing section grows an invoices view, and suddenly a user three levels down has no cheap way to climb back up. The usual fix — a hand-rolled row of <a> tags with chevrons typed between them — works until the fourth page needs it, at which point the markup has been copied three times with three different gap values and no consistent accessibility story.
Drivn's Breadcrumb replaces that with one component you install once and own outright. The CLI writes a single breadcrumb.tsx into your project: a root <nav> that walks its children and injects separators automatically, plus Breadcrumb.Item, Breadcrumb.Page, Breadcrumb.Separator, and Breadcrumb.Ellipsis hanging off it via dot notation. There are no runtime UI dependencies — the file imports React, your local cn helper, and two Lucide icons, nothing more.
This guide adds the Breadcrumb to a React app you already have: check the prerequisites, install through the CLI, render a first trail, customize the separator and collapse deep paths, then derive the trail from your router's location. The steps work the same in Vite, Remix, or any React-plus-Tailwind setup. App Router specifics live in the Next.js Breadcrumb guide, and finished patterns are collected on the Breadcrumb examples page.
Prerequisites
Three things need to be true before the install. Tailwind CSS v4 must be processing your styles — every visual decision in the component is a utility class, from the flex items-center gap-1.5 list layout to the muted separator color. TypeScript must be configured, because the file ships as .tsx and types its sub-components against React.AnchorHTMLAttributes; Drivn treats TypeScript as part of the stack, never an option. And the @/ import alias must resolve to your source directory, since the component imports cn from @/utils/cn. Unlike some Drivn components, the Breadcrumb also needs lucide-react installed — it pulls ChevronRight for the default separator and MoreHorizontal for the ellipsis. The installation page covers the minimal setup for hand-rolled projects.
Step 2 — Render your first trail
Import the root and compose the trail from Breadcrumb.Item for every clickable ancestor and a single Breadcrumb.Page for where the user currently is. Notice what you do not write: separators. The root walks its children with React.Children.toArray(children).flatMap() and inserts a Breadcrumb.Separator before every child after the first, skipping any separator you placed yourself. The rendered HTML is the structure assistive tech expects — a <nav aria-label="Breadcrumb"> wrapping an <ol>, each link inside an <li>, and the current page marked aria-current="page" and rendered as a non-interactive <span>. Hover styling comes from hover:text-foreground on the link class, and the whole list wraps gracefully on narrow screens via flex-wrap. The Breadcrumb docs cover the full sub-component list and props.
1 import { Breadcrumb } from '@/components/ui/breadcrumb' 2 3 <Breadcrumb> 4 <Breadcrumb.Item href="/">Home</Breadcrumb.Item> 5 <Breadcrumb.Item href="/settings">Settings</Breadcrumb.Item> 6 <Breadcrumb.Page>Billing</Breadcrumb.Page> 7 </Breadcrumb>
Step 3 — Swap the separator and collapse deep trails
The default separator is a ChevronRight, dimmed via text-muted-foreground/60 and sized by the [&>svg]:size-3.5 rule in the component's styles. To change it everywhere at once, pass any React node to the root's separator prop — a slash for file-path hierarchies, a dot for compact layouts, an icon from another set. Every auto-inserted separator uses the node you pass, so one prop keeps the whole trail consistent. For hierarchies that run deep, Breadcrumb.Ellipsis collapses the middle: it renders a MoreHorizontal icon with a visually hidden "More pages" label, so screen readers still announce that intermediate levels exist. Keep the root and the current page visible on both sides of the ellipsis — users orient on the ends of a trail, not its middle.
1 // one prop swaps every separator 2 <Breadcrumb separator={<span>/</span>}> 3 <Breadcrumb.Item href="/">Home</Breadcrumb.Item> 4 <Breadcrumb.Item href="/files">Files</Breadcrumb.Item> 5 <Breadcrumb.Page>report.pdf</Breadcrumb.Page> 6 </Breadcrumb> 7 8 // collapse the middle of a deep trail 9 <Breadcrumb> 10 <Breadcrumb.Item href="/">Home</Breadcrumb.Item> 11 <Breadcrumb.Ellipsis /> 12 <Breadcrumb.Item href="/settings/billing"> 13 Billing 14 </Breadcrumb.Item> 15 <Breadcrumb.Page>Invoices</Breadcrumb.Page> 16 </Breadcrumb>
Step 4 — Derive the trail from your router
Static trails suit fixed pages, but most React apps want the breadcrumb computed from the current URL. With react-router, read pathname from useLocation, split it on /, and map the segments: each prefix becomes a Breadcrumb.Item with an accumulated href, and the final segment renders as Breadcrumb.Page. A small labels record turns URL slugs into readable titles. Because the auto-injection flattens mapped arrays before inserting separators, the dynamic version needs no special handling. One more copy-and-own note: Breadcrumb.Item renders a plain <a>, which triggers a full reload on click. For client-side transitions, open breadcrumb.tsx and swap the anchor for your router's <Link> — one edit, every call site upgraded. The examples page shows the Next.js flavor of this same pattern.
1 import { useLocation } from 'react-router-dom' 2 import { Breadcrumb } from '@/components/ui/breadcrumb' 3 4 const labels: Record<string, string> = { 5 settings: 'Settings', 6 billing: 'Billing', 7 invoices: 'Invoices', 8 } 9 10 export function RouteBreadcrumb() { 11 const { pathname } = useLocation() 12 const segments = pathname.split('/').filter(Boolean) 13 14 return ( 15 <Breadcrumb> 16 <Breadcrumb.Item href="/">Home</Breadcrumb.Item> 17 {segments.map((segment, i) => { 18 const href = '/' + segments.slice(0, i + 1).join('/') 19 const label = labels[segment] ?? segment 20 const isLast = i === segments.length - 1 21 22 return isLast ? ( 23 <Breadcrumb.Page key={href}>{label}</Breadcrumb.Page> 24 ) : ( 25 <Breadcrumb.Item key={href} href={href}> 26 {label} 27 </Breadcrumb.Item> 28 ) 29 })} 30 </Breadcrumb> 31 ) 32 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
Frequently asked questions
Yes. The component has no router coupling — items render plain anchors, so any routing setup works out of the box with full page loads. For soft client-side transitions, edit the Item function inside your copy of breadcrumb.tsx and replace the <a> with react-router's <Link to={href}>. Because the file lives in your repository, the edit applies to every breadcrumb in the app and no upstream release can undo it.
The root inserts them for you at render time. It converts its children with React.Children.toArray, then prepends a Breadcrumb.Separator before every child after the first — unless that child already is a separator you placed manually. The inserted separators render whatever you passed to the root's separator prop, falling back to a ChevronRight icon, so the trail stays visually uniform without per-position markup.
Yes — the accessible structure is the component's default output, not an opt-in. The root is a <nav aria-label="Breadcrumb"> containing an ordered list, the current page carries aria-current="page", every separator is wrapped in role="presentation" with aria-hidden="true", and the ellipsis pairs its icon with screen-reader-only "More pages" text. That matches the WAI-ARIA breadcrumb pattern as published.
Every Drivn component ships as a .tsx source file — the registry is TypeScript-only by design. The Breadcrumb types its Item props against React.AnchorHTMLAttributes<HTMLAnchorElement> so anchor attributes pass through with full checking, and the sub-components attach to the root through Object.assign with their signatures intact. A JavaScript project cannot compile the file as shipped, so TypeScript is a prerequisite, not a preference.

