Drivn vs shadcn/ui — Breadcrumb Compared
Side-by-side comparison of Drivn and shadcn/ui React Breadcrumb — API surface, runtime deps, auto-separator injection, and accessibility defaults.
A breadcrumb trail is a row of links and a divider glyph — close to the smallest interactive component either library ships. That smallness is what makes it a clean test of composition philosophy: with almost no behavior to hide behind, the API shape is the whole product. Drivn's Breadcrumb and shadcn/ui's Breadcrumb both arrive as TypeScript source you paste into your own repo, and both render the same semantic markup, yet writing a three-level trail in each produces visibly different JSX.
shadcn/ui splits the component into seven named exports — Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, and BreadcrumbEllipsis. You import the pieces you need, nest them in the required order, and drop a <BreadcrumbSeparator /> by hand between every pair of items. BreadcrumbLink accepts asChild through @radix-ui/react-slot, so swapping in a Next.js <Link> costs one prop.
Drivn attaches every piece to a single root with Object.assign: Breadcrumb.Item, Breadcrumb.Page, Breadcrumb.Separator, Breadcrumb.Ellipsis. The root walks its own children and inserts the separators for you, so the JSX holds nothing but the trail. Nothing outside React, Tailwind, and two Lucide icons gets imported.
What follows is the difference measured on code rather than claims: import surface, separator handling, dependency cost, accessibility defaults, and the one capability Drivn deliberately leaves out.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Runtime UI dependencies | None (React + Tailwind) | @radix-ui/react-slot |
| API style | Dot notation (1 import) | 7 named exports |
| Separator between items | Auto-injected | Manual per position |
| Custom separator | separator prop on root | Per-child override |
| asChild / polymorphic link | ||
| Container element | nav > ol > li | nav > ol > li |
| aria-current on current page | ||
| Ellipsis component | ||
| Typical LOC for 3-level breadcrumb | ~5 | ~12 |
| License | MIT | MIT |
Import shape: seven exports vs dot notation
shadcn/ui's Breadcrumb is a tree of co-dependent exports. A typical usage pulls Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, and BreadcrumbSeparator from the same file, then nests them in a specific order. The import statement alone runs past eighty characters, and refactoring a route means editing six places that all reference the same component family.
Drivn's Breadcrumb uses one named import. Every sub-component hangs off the root via Object.assign, so TypeScript autocompletes Breadcrumb.Item, Breadcrumb.Page, and Breadcrumb.Separator after you type the dot. The same hierarchy is visible in the JSX, but the import and the parent-child relationship live in a single place. Smaller diffs on refactor, less import churn, and no risk of importing BreadcrumbLink from the wrong file in a monorepo with multiple component libraries.
1 // shadcn/ui — seven exports for one component family 2 import { 3 Breadcrumb, 4 BreadcrumbList, 5 BreadcrumbItem, 6 BreadcrumbLink, 7 BreadcrumbPage, 8 BreadcrumbSeparator, 9 } from '@/components/ui/breadcrumb' 10 11 <Breadcrumb> 12 <BreadcrumbList> 13 <BreadcrumbItem> 14 <BreadcrumbLink href="/">Home</BreadcrumbLink> 15 </BreadcrumbItem> 16 <BreadcrumbSeparator /> 17 <BreadcrumbItem> 18 <BreadcrumbPage>Docs</BreadcrumbPage> 19 </BreadcrumbItem> 20 </BreadcrumbList> 21 </Breadcrumb> 22 23 // Drivn — one import, dot notation 24 import { Breadcrumb } from '@/components/ui/breadcrumb' 25 26 <Breadcrumb> 27 <Breadcrumb.Item href="/">Home</Breadcrumb.Item> 28 <Breadcrumb.Page>Docs</Breadcrumb.Page> 29 </Breadcrumb>
Automatic separator injection
In shadcn/ui the separator is markup you author. After every BreadcrumbItem except the last you place a <BreadcrumbSeparator />, so a three-level trail is five children and a five-level trail is nine. The count is mechanical, which is exactly why it breaks: someone inserts a level, forgets the divider, and two crumbs end up touching with no chevron between them.
Drivn's root does the insertion instead. It walks React.Children.toArray(children).flatMap() and, for every child past index zero that is not already a Breadcrumb.Separator, emits a separator before it. That check matters — it keeps the escape hatch open. Hand-place a <Breadcrumb.Separator> yourself and the root sees it and skips its automatic one, so a single position can carry a different glyph than the rest of the trail. The list itself is a plain <ol> styled with flex items-center gap-1.5 flex-wrap text-sm text-muted-foreground, so a deep trail wraps to a second line rather than overflowing its container.
1 // Drivn — write items only, separators auto-inject 2 <Breadcrumb> 3 <Breadcrumb.Item href="/">Home</Breadcrumb.Item> 4 <Breadcrumb.Item href="/docs">Docs</Breadcrumb.Item> 5 <Breadcrumb.Item href="/docs/components"> 6 Components 7 </Breadcrumb.Item> 8 <Breadcrumb.Page>Breadcrumb</Breadcrumb.Page> 9 </Breadcrumb>
Runtime dependencies and bundle footprint
shadcn/ui's Breadcrumb pulls @radix-ui/react-slot for the asChild prop on BreadcrumbLink. Slot is a small primitive but it is still a runtime dependency your bundler resolves and your package.json records. Across a full shadcn install that uses many components with asChild, Slot becomes one of the most frequently-imported pieces.
Drivn's Breadcrumb has zero runtime UI dependencies. The source imports React, the local cn() utility, and two Lucide icons — ChevronRight for the default separator and MoreHorizontal for the ellipsis. The whole file is just over a hundred lines, including the four sub-components and the styles object. If you need to render the link as a Next.js <Link>, you edit the local Item function in src/components/ui/breadcrumb.tsx after install and swap the <a> for a <Link>. For most breadcrumb patterns — static site nav, docs trees, settings pages — that is a one-line edit you make once.
Custom separators
Both libraries ship a ChevronRight as the default separator. Replacing it is where the APIs diverge. shadcn/ui's BreadcrumbSeparator renders whatever children you pass — a chevron, a slash, a dot — but because every separator in the tree is a separate JSX node, you pass the same child into each one or wrap the separator in your own component that always injects the same glyph.
Drivn's Breadcrumb root accepts a separator prop. Pass <span>/</span> once on the root and every auto-injected separator uses the slash. The prop is typed as React.ReactNode, so any JSX works — an icon from a different set, a styled character, a branded element. Changing the trail's glyph across a whole app is a single prop override at the root layout.
1 // One prop controls every separator in the list 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>
Accessibility defaults
Both libraries get the core accessibility right. The root renders as a <nav> with aria-label="Breadcrumb", the list uses <ol> for ordered semantics, each link sits inside an <li>, and the current page has aria-current="page" so screen readers announce it distinctly from the links.
Drivn's Breadcrumb also marks separators as aria-hidden="true" with role="presentation" so assistive tech ignores the visual dividers. The ellipsis component wraps its icon with an <span className="sr-only">More pages</span> so users navigating with a screen reader hear a meaningful label instead of silence. shadcn/ui's Breadcrumb handles the same patterns, and the two implementations pass the same axe and Lighthouse checks. The accessibility story is a wash — both are correct by default. The difference lives elsewhere, in composition surface and runtime footprint.
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
Migrate if you want to drop @radix-ui/react-slot from your dependency tree, prefer a single dot-notation import over seven separate named exports, or like the automatic separator injection that removes redundant JSX. Stay on shadcn if you rely on the asChild prop to polymorphically render BreadcrumbLink as a framework router link, or your team has standardized on shadcn's multi-export pattern across every component. Both libraries use the same semantic HTML and accessibility defaults, so migrated call sites read nearly identically.
The root component calls React.Children.toArray(children).flatMap() over its children and, for every child past the first, prepends a Breadcrumb.Separator element before it. The separator passes the root-level separator prop as its children, so the glyph is consistent across the whole list. You write only the items and the current page inside <Breadcrumb>; a three-level breadcrumb is three JSX children instead of the five shadcn needs with manual separator placement.
Yes, but the cleanest path is to edit the local component rather than wrap. After running drivn add breadcrumb, open src/components/ui/breadcrumb.tsx and change the <a> tag inside the Item function to a <Link> from next/link. The href prop and the spread {...props} transfer cleanly because LinkProps extends the same anchor attribute types. Every breadcrumb in the app then uses client-side routing with no call-site changes.
Pass a separator prop to the root component. The prop is typed as React.ReactNode, so you can pass a slash (<span>/</span>), an icon from any library (<ArrowRight className="w-3 h-3" />), or a styled element with custom tokens. The root hands the node to every auto-injected Breadcrumb.Separator at render time, so one override applies everywhere. This differs from shadcn/ui, which requires you to either pass children into every BreadcrumbSeparator or wrap it in a custom component.
Yes. The root renders as <nav aria-label="Breadcrumb"> with an <ol> list for ordered semantics. Each item is an <li> with an <a> child; the current page renders as <li aria-current="page"> with a non-interactive <span> so assistive tech announces it as the location. The auto-injected separators have role="presentation" and aria-hidden="true", and the Breadcrumb.Ellipsis includes a visually hidden "More pages" label for screen readers. The accessibility profile matches shadcn/ui's implementation.

