Basic Breadcrumb
Original · freebasic breadcrumb trail
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/crumb-basic.json"use client";
import { useId, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Crumb = {
label: string;
href: string;
};
type Trail = {
id: string;
name: string;
description: string;
crumbs: Crumb[];
};
const TRAILS: Trail[] = [
{
id: "docs",
name: "Documentation",
description:
"A compact navigation trail that shows where the current page sits in the site hierarchy and lets people jump back to any ancestor level in a single click.",
crumbs: [
{ label: "Home", href: "#/" },
{ label: "Documentation", href: "#/docs" },
{ label: "Components", href: "#/docs/components" },
{ label: "Navigation", href: "#/docs/components/navigation" },
{ label: "Breadcrumbs", href: "#/docs/components/navigation/breadcrumbs" },
],
},
{
id: "blog",
name: "Engineering blog",
description:
"How we rebuilt our primary navigation around semantic landmarks, roving focus, and visible focus states — and what the accessibility audit taught us along the way.",
crumbs: [
{ label: "Home", href: "#/" },
{ label: "Blog", href: "#/blog" },
{ label: "Engineering", href: "#/blog/engineering" },
{ label: "Building Accessible Navigation", href: "#/blog/engineering/accessible-nav" },
],
},
{
id: "market",
name: "Marketplace",
description:
"Every release note for the Nebula admin template, from the initial 1.0 launch through the latest performance passes and refined dark-mode surfaces.",
crumbs: [
{ label: "Home", href: "#/" },
{ label: "Marketplace", href: "#/market" },
{ label: "UI Kits", href: "#/market/ui-kits" },
{ label: "Nebula Dashboard", href: "#/market/ui-kits/nebula" },
{ label: "Changelog", href: "#/market/ui-kits/nebula/changelog" },
],
},
];
function HomeIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M3 10.5 12 4l9 6.5" />
<path d="M5 9.4V20h5v-5.5h4V20h5V9.4" />
</svg>
);
}
function ChevronSep() {
return (
<svg
viewBox="0 0 24 24"
className="mx-1 h-4 w-4 shrink-0 text-slate-300 dark:text-slate-600 sm:mx-1.5"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m9 6 6 6-6 6" />
</svg>
);
}
function DotsIcon() {
return (
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<circle cx="5" cy="12" r="1.6" />
<circle cx="12" cy="12" r="1.6" />
<circle cx="19" cy="12" r="1.6" />
</svg>
);
}
const linkClass =
"inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 text-slate-500 transition-colors hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";
function CrumbLink({ crumb, withIcon }: { crumb: Crumb; withIcon?: boolean }) {
return (
<a href={crumb.href} className={linkClass}>
{withIcon ? <HomeIcon /> : null}
<span>{crumb.label}</span>
</a>
);
}
function CurrentCrumb({ label }: { label: string }) {
return (
<span
aria-current="page"
className="inline-flex items-center rounded-md px-1.5 py-1 font-medium text-slate-900 dark:text-slate-50"
>
{label}
</span>
);
}
export default function CrumbBasic() {
const reduce = useReducedMotion();
const groupLabelId = useId();
const [trailId, setTrailId] = useState<string>(TRAILS[0].id);
const [expanded, setExpanded] = useState<boolean>(false);
const selected = TRAILS.find((t) => t.id === trailId) ?? TRAILS[0];
const crumbs = selected.crumbs;
const collapsible = crumbs.length > 4;
const head = crumbs[0];
const middle = crumbs.slice(1, -2);
const tail = crumbs.slice(-2);
const currentLabel = crumbs[crumbs.length - 1].label;
function selectTrail(id: string) {
setTrailId(id);
setExpanded(false);
}
return (
<section className="relative w-full bg-slate-50 px-6 py-20 dark:bg-slate-950 sm:py-28">
<style>{`
@keyframes crumbbasic-grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.crumbbasic-underline {
transform-origin: left center;
animation: crumbbasic-grow 620ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes crumbbasic-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.82); }
}
.crumbbasic-dot {
animation: crumbbasic-pulse 2.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.crumbbasic-underline,
.crumbbasic-dot {
animation: none !important;
}
}
`}</style>
<div className="mx-auto max-w-3xl">
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-8">
<nav aria-label="Breadcrumb">
<ol className="flex flex-wrap items-center text-sm">
{collapsible ? (
<>
<li className="flex items-center">
<CrumbLink crumb={head} withIcon />
</li>
<li className="flex items-center">
<ChevronSep />
<button
type="button"
onClick={() => setExpanded((v) => !v)}
aria-expanded={expanded}
className={`inline-flex h-7 items-center rounded-md px-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
expanded
? "bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-100"
: "text-slate-500 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
}`}
>
<DotsIcon />
<span className="sr-only">
{expanded
? `Hide ${middle.length} intermediate pages`
: `Show ${middle.length} hidden pages`}
</span>
</button>
</li>
<AnimatePresence initial={false}>
{expanded
? middle.map((crumb) => (
<motion.li
key={crumb.href}
className="flex items-center"
initial={reduce ? false : { opacity: 0, x: -6 }}
animate={reduce ? {} : { opacity: 1, x: 0 }}
exit={reduce ? {} : { opacity: 0, x: -6 }}
transition={{ duration: reduce ? 0 : 0.18, ease: "easeOut" }}
>
<ChevronSep />
<CrumbLink crumb={crumb} />
</motion.li>
))
: null}
</AnimatePresence>
{tail.map((crumb, idx) => (
<li key={crumb.href} className="flex items-center">
<ChevronSep />
{idx === tail.length - 1 ? (
<CurrentCrumb label={crumb.label} />
) : (
<CrumbLink crumb={crumb} />
)}
</li>
))}
</>
) : (
crumbs.map((crumb, idx) => (
<li key={crumb.href} className="flex items-center">
{idx > 0 ? <ChevronSep /> : null}
{idx === crumbs.length - 1 ? (
<CurrentCrumb label={crumb.label} />
) : (
<CrumbLink crumb={crumb} withIcon={idx === 0} />
)}
</li>
))
)}
</ol>
</nav>
<div className="mt-7">
<p className="flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
<span
className="crumbbasic-dot h-1.5 w-1.5 rounded-full bg-indigo-500"
aria-hidden="true"
/>
Current page
</p>
<h1 className="mt-2 text-2xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-3xl">
{currentLabel}
</h1>
<span
key={trailId}
className="crumbbasic-underline mt-3 block h-0.5 w-16 rounded-full bg-indigo-500"
aria-hidden="true"
/>
<p className="mt-4 max-w-prose text-sm leading-relaxed text-slate-600 dark:text-slate-300">
{selected.description}
</p>
</div>
</div>
<div className="mt-6">
<p
id={groupLabelId}
className="mb-2 text-xs font-medium text-slate-500 dark:text-slate-400"
>
Preview another path
</p>
<div role="group" aria-labelledby={groupLabelId} className="flex flex-wrap gap-2">
{TRAILS.map((t) => {
const active = t.id === trailId;
return (
<button
key={t.id}
type="button"
aria-pressed={active}
onClick={() => selectTrail(t.id)}
className={`rounded-lg px-3.5 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-950 ${
active
? "bg-slate-900 text-white dark:bg-white dark:text-slate-900"
: "border border-slate-200 bg-white text-slate-600 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
}`}
>
{t.name}
</button>
);
})}
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Slash Breadcrumb
Originalslash separator breadcrumbs

Chevron Breadcrumb
Originalchevron separator breadcrumbs

Arrow Breadcrumb
Originalarrow-chip breadcrumbs

Pill Breadcrumb
Originalpill breadcrumbs
Icons Breadcrumb
Originalbreadcrumbs with icons

Collapsed Breadcrumb
Originalcollapsed breadcrumbs with ellipsis

Dropdown Breadcrumb
Originalbreadcrumb with a dropdown level

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

