A breadcrumb is a labelled nav around an ordered list — ancestor links, current-page text, and a collapse folding the middle
Home / Products / Electronics / Laptops. That little trail lives at the top of every store, docs site and file manager, and I always half-assumed it needed a component library. It doesn't. A breadcrumb is a semantic back
Home / Products / Electronics / Laptops. That little trail lives at the top of every store, docs site and file manager, and I always half-assumed it needed a component library. It doesn't. A breadcrumb is a semantic backbone plus one clever collapse, and once I built it from scratch — no framework — the whole recipe fit in my head. Here's the version I keep now.
The skeleton is nav > ol > li, and the ol matters
A breadcrumb is navigation, so it lives in a <nav> — labelled, because a page can have several. Inside goes an ordered list, not a <ul>: the sequence is the hierarchy (Home comes before Products comes before Laptops), and <ol> is how you say "order carries meaning" in HTML.
<nav aria-label="Breadcrumb">
<ol>
<li>…</li> <!-- Home -->
<li>…</li> <!-- Products -->
<li>…</li> <!-- Laptops (current) -->
</ol>
</nav>
Links for ancestors, plain text for where you are
Every level except the last is somewhere you can go, so it's a real <a href>. The last item is the page you're already on — linking to yourself is pointless — so it's a <span>, and it carries the one attribute that makes a breadcrumb accessible: aria-current="page". Exactly one item ever gets it.
<li><a href="/products">Products</a></li>
<li><span aria-current="page">Laptops</span></li>
Separators are decoration — hide them from screen readers
The / or › between items is purely visual. Typed as real text, a screen reader announces "Home slash Products slash Laptops" — noise. So either draw it with CSS ::before (it never enters the accessibility tree) or mark each separator aria-hidden="true". Either way the announcement stays clean: "Home, link. Products, link. Laptops, current page."
Style it low-key with flex-wrap
Lay the <ol> out with display:flex and kill the default bullets and margins, then add flex-wrap:wrap so a long trail drops to a second line on a narrow screen instead of forcing a horizontal scrollbar. Give links a hover state and a comfortable click target, and make the current page a touch heavier so the eye lands on "where am I" first. A breadcrumb is a wayfinding aid, not a headline — keep it quiet.
Render from data — the trail is just an array
In a real app you don't hand-write the <li>s. You keep the trail as an array of { label, href } and map over it; the rule for "which is current" is simply the last index. Now the whole thing is a pure function of that array, which is what lets clicking the site map rebuild it instantly. Climbing back up is one line — trail.slice(0, i + 1):
ol.addEventListener("click", (e) => {
const link = e.target.closest("[data-index]");
if (!link) return;
const i = +link.dataset.index;
trail = trail.slice(0, i + 1); // climb to that ancestor
render();
});
Collapse a long trail — protect the ends, fold the middle
Deep hierarchies overflow, and this is the part everyone gets wrong. When the trail is longer than a threshold, keep the first item (Home — the anchor you can always return to) and the last few (the context right around you), and replace the middle with a single …. The two ends carry the most meaning:
function collapse(trail, max) {
if (trail.length <= max) return { visible: trail, hidden: [] };
return {
head: trail.slice(0, 1), // keep Home
hidden: trail.slice(1, trail.length - (max - 2)), // the fold
tail: trail.slice(trail.length - (max - 2)), // keep the last few
};
}
The folded ancestors can't just vanish — they're still navigation targets — so the … is a real <button> with aria-haspopup and aria-expanded, opening a role="menu" of the hidden items. Wire the keyboard: arrows move between menu items, Esc closes and returns focus to the button, a click outside dismisses. Never kill the :focus-visible ring.
That's the whole thing: a labelled <nav>, an <ol> because order is the hierarchy, links up, text here, silent separators, and a collapse that protects Home and Current. Wrap the same data in schema.org BreadcrumbList and search engines render the trail in results for free.
Play with it — click the site map, hit Add, drag the collapse threshold, open the overflow menu:
https://dev48v.infy.uk/design/day47-breadcrumb.html
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.