Dev.to WebDev 🛠 Dev 👁 0 📖 3 min read

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

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.