DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

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 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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
  };
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (1)

Collapse
 
topstar_ai profile image
Luis Cruz

I appreciate how you broke down the anatomy of a breadcrumb navigation, particularly the use of an ordered list (<ol>) to convey the hierarchical structure. The distinction between using links for ancestors and plain text for the current page is also well-explained, and the tip about hiding separators from screen readers to maintain a clean announcement is valuable. Your implementation of a collapsible breadcrumb trail, protecting the first and last items while folding the middle, is quite elegant. How do you suggest handling cases where the breadcrumb trail needs to be dynamically updated based on user interactions, such as when a user navigates through a complex application with multiple nested views?