Breadcrumbs — Collapsing Trail HTML CSS JS Snippet
Breadcrumb Trail with Collapsing Middle Items · Navigation · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="demo">
<nav class="crumbs" id="crumbsShort" aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li><span class="sep">/</span><a href="#">Settings</a></li>
<li><span class="sep">/</span><span class="current" aria-current="page">Profile</span></li>
</ol>
</nav>
<nav class="crumbs" id="crumbsLong" aria-label="Breadcrumb">
<ol id="crumbsLongList"></ol>
</nav>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f8f9fa; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; }
.demo { width: 100%; max-width: 460px; display: flex; flex-direction: column; gap: 20px; }
.crumbs ol { list-style: none; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: 13.5px; }
.crumbs li { display: flex; align-items: center; gap: 4px; }
.crumbs a { color: #6b7280; text-decoration: none; padding: 3px 4px; border-radius: 5px; }
.crumbs a:hover { color: #2563eb; background: #f0f5ff; }
.crumbs .sep { color: #c7cbd1; }
.crumbs .current { color: #111827; font-weight: 600; padding: 3px 4px; }
.crumbs .collapse-btn { background: #f3f4f6; border: none; border-radius: 5px; color: #6b7280; font-size: 13px; font-weight: 700; width: 24px; height: 22px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.crumbs .collapse-btn:hover { background: #e5e7eb; color: #374151; }var LONG_PATH = ['Home', 'Workspace', 'Projects', 'Marketing Site', 'Pages', 'Landing', 'Hero Section', 'Variant B'];
function renderLong() {
var list = document.getElementById('crumbsLongList');
var collapsed = window.innerWidth < 480 && LONG_PATH.length > 4;
var items = LONG_PATH;
var html = '';
if (collapsed) {
var head = LONG_PATH.slice(0, 1);
var tail = LONG_PATH.slice(-2);
html += crumbHtml(head[0], 0, false);
html += '<li><span class="sep">/</span><button class="collapse-btn" onclick="expandCrumbs()" aria-label="Show hidden path segments">\u2026</button></li>';
tail.forEach(function(label, i) {
var isLast = i === tail.length - 1;
html += crumbHtml(label, i, isLast, true);
});
} else {
items.forEach(function(label, i) {
var isLast = i === items.length - 1;
html += crumbHtml(label, i, isLast, i > 0);
});
}
list.innerHTML = html;
}
function crumbHtml(label, index, isLast, withSep) {
var sep = withSep ? '<span class="sep">/</span>' : '';
if (isLast) return '<li>' + sep + '<span class="current" aria-current="page">' + label + '</span></li>';
return '<li>' + sep + '<a href="#">' + label + '</a></li>';
}
function expandCrumbs() {
var list = document.getElementById('crumbsLongList');
var html = '';
LONG_PATH.forEach(function(label, i) {
var isLast = i === LONG_PATH.length - 1;
html += crumbHtml(label, i, isLast, i > 0);
});
list.innerHTML = html;
}
renderLong();
window.addEventListener('resize', renderLong);Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Breadcrumb Trail — aria-current Navigation with Collapsing Middle Segments

Breadcrumbs show a user where they are in a site's hierarchy and let them jump back up to any ancestor level in one click — standard on any site with nested categories, file paths, or multi-step settings. This snippet covers both the simple three-level case and the harder problem: what happens when the path is eight levels deep and doesn't fit on one line.
Semantic structure
The trail is a <nav aria-label="Breadcrumb"> wrapping an <ol> — an ordered list, not an unordered one, because a breadcrumb path has a meaningful sequence. Every visited ancestor is a real <a href="...">, and only the final, current segment is a plain <span> with aria-current="page" — marking the current location as non-interactive is correct because linking a page to itself serves no purpose and aria-current tells assistive technology which segment represents "here" without needing to infer it from position.
Separators as siblings, not pseudo-elements
Each / separator is a real <span class="sep"> element placed before its link, rather than a ::before CSS pseudo-element. This is a deliberate accessibility choice: CSS-generated content is inconsistently exposed to screen readers across browsers, so a visible, real DOM separator guarantees consistent behavior, at the minor cost of one extra element per crumb.
The collapsing problem
A breadcrumb trail with eight segments doesn't fit in a typical sidebar or mobile viewport width. This snippet's renderLong() checks window.innerWidth < 480 && LONG_PATH.length > 4 and, when both are true, renders only the first segment, a … button standing in for the hidden middle, and the last two segments — the same convention Windows Explorer and macOS Finder's breadcrumb bars use. The first segment stays because it anchors the user to the top of the hierarchy; the last two stay because they show exactly where the user currently is and the level directly above it.
The collapse button is interactive, not decorative
The … is a real <button>, not static text, with aria-label="Show hidden path segments" — clicking it calls expandCrumbs(), which re-renders the full uncollapsed list. A common mistake is rendering the ellipsis as plain text, which discards the ability to ever see the hidden segments; making it a button preserves full navigability while still solving the width problem.
Recalculating on resize
window.addEventListener('resize', renderLong) means resizing the browser (or rotating a device) re-evaluates the collapse condition and re-renders — a trail collapsed on a narrow mobile viewport automatically expands back to full when the window widens past the 480px threshold, without requiring a page reload.
Building the HTML string safely
crumbHtml() is the single function responsible for rendering one segment, called identically whether building the collapsed or expanded view — this avoids duplicating the link-vs-current-span branching logic in two places, which is a common source of the two views silently drifting apart from each other over time.
Truncating long individual labels
This snippet handles too many *segments*; a segment with a very long individual label (a long file name, for instance) is a separate problem, best solved with text-overflow: ellipsis and a max-width on that specific <a> or <span>, combined with a native title attribute so the full text is still available on hover.
React integration
In React, pass the path as an array of { label, href } objects, derive the collapsed/expanded view with a small useMemo keyed on a window.innerWidth value tracked via a resize listener in useEffect, and render the last item as a <span aria-current="page"> exactly as this snippet does.
See also the breadcrumb dropdown for a variant where each segment itself opens a dropdown of sibling pages, and the table of contents snippet for a different kind of "where am I" navigation aid.
Step by step
How to Use
- 1Copy the nav/ol structureUse <nav aria-label="Breadcrumb"><ol>...</ol></nav> with one <li> per path segment — links for ancestors, a plain aria-current span for the current page.
- 2Replace the LONG_PATH arraySwap LONG_PATH for your real route hierarchy, either hardcoded per page or generated from your router's current path segments.
- 3Add the CSSPaste the CSS once — it styles both the short static example and the dynamically rendered long trail identically.
- 4Adjust the collapse thresholdChange the 480px width check and the "> 4 segments" condition in renderLong() to match when your layout actually needs to collapse.
- 5Wire real hrefsReplace the demo's href="#" placeholders with real ancestor URLs so each breadcrumb link actually navigates.
Real-world uses
Common Use Cases
nav and ol markup with aria-current on the final item.Got questions?
Frequently Asked Questions
A link to the page the user is already on serves no purpose and can confuse screen reader users into thinking it navigates somewhere new. Using a plain span with aria-current="page" correctly marks it as the non-interactive current location.
Edit the condition in renderLong() — currently window.innerWidth < 480 && LONG_PATH.length > 4 — and the slice indices in the collapsed branch (head = first 1, tail = last 2) to whatever combination fits your layout.
CSS-generated content is not consistently exposed to assistive technology across all browsers and screen readers. A real <span class="sep"> guarantees the separator (or its absence, since it can be hidden from screen readers with aria-hidden if desired) behaves predictably everywhere.
Split your current route pathname on "/", map each segment to a { label, href } pair by accumulating the path so far for each href, and optionally translate URL slugs into human-readable labels via a lookup table or your CMS data.
Open the Export menu on the snippet page for a React component accepting a path array as a prop with the same collapse-on-resize logic in a useEffect, a React + Tailwind version, a Vue 3 SFC, or an Angular standalone component with an @Input() path array.
Yes. While the Breadcrumb Trail with Collapsing Middle Items snippet gives you one pattern to copy, the free Responsive Navbar Builder at webdevpuneet.com/navbar-builder/ designs a whole navigation bar: logo, links and dropdown menus, a mobile hamburger menu (dropdown panel, slide-in drawer or full screen) and a scroll effect (sticky, shrink, hide on scroll or transparent). It has six presets, a live preview that runs the real code, and exports HTML + CSS + vanilla JS, React, Tailwind, Vue 3 or Angular — or you can click Fork & Edit to keep editing it in My Code.