Breadcrumbs — Collapsing Trail HTML CSS JS Snippet

Breadcrumb Trail with Collapsing Middle Items · Navigation · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Semantic <nav><ol> structure with aria-current="page" marking the non-interactive final segment
Real DOM separator elements instead of CSS-generated content, for consistent screen-reader behavior
Automatic collapsing of middle segments on narrow viewports, matching the OS file-browser convention
Collapse ellipsis is a real interactive button, not static text — clicking it reveals the full path
Recalculates and re-renders on window resize, expanding back automatically on wider viewports
Single shared render function for both collapsed and expanded views, preventing the two from drifting apart
Hover and focus states on every ancestor link
Zero dependencies — pure HTML, CSS, and JavaScript

About this UI Snippet

Breadcrumb Trail — aria-current Navigation with Collapsing Middle Segments

Screenshot of the Breadcrumb Trail with Collapsing Middle Items snippet rendered live

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this breadcrumb trail's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why the current page renders as a plain span with aria-current instead of a link, and how the collapse-to-ellipsis logic decides which segments to keep versus hide on a narrow viewport. Since this snippet builds its path from a hardcoded array, it's a good candidate to ask the assistant to wire up to your actual router — describe your routing library and have it generate the segment array (with real hrefs and human-readable labels) from the current URL automatically. Beyond that, ask it to add a dropdown on the collapse button showing the hidden segments as a menu instead of fully expanding the trail, or to add individual-segment text truncation for very long labels.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an accessible breadcrumb trail component in plain HTML, CSS, and JavaScript, no framework, no libraries.

Requirements:
- Use a semantic nav element labeled as a breadcrumb, wrapping an ordered list where each list item represents one level of a page hierarchy. Every ancestor level must be a real link; the final, current-page segment must be plain non-interactive text marked with an aria-current attribute indicating it is the current page, not a link to itself.
- Render separators between segments as real visible DOM elements rather than CSS-generated pseudo-element content.
- Given a path with more than four segments, automatically collapse the middle segments into a single interactive ellipsis button when the viewport is narrow (for example under 480px wide), showing only the first segment, the collapse button, and the last two segments. On a wider viewport, render the full uncollapsed path instead.
- The collapse ellipsis must be a real button (not static punctuation) with an appropriate accessible label, and clicking it must re-render the full, uncollapsed path in place.
- Recalculate whether to collapse or expand whenever the browser window is resized, so a trail collapsed on a narrow width automatically expands again if the window is widened past the threshold, without requiring a page reload.
- Use one shared rendering function for building both the collapsed and the fully expanded views so the logic for "is this the last segment" and "is this a link or the current-page text" is not duplicated in two separate code paths.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Copy 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.
  2. 2
    Replace the LONG_PATH arraySwap LONG_PATH for your real route hierarchy, either hardcoded per page or generated from your router's current path segments.
  3. 3
    Add the CSSPaste the CSS once — it styles both the short static example and the dynamically rendered long trail identically.
  4. 4
    Adjust the collapse thresholdChange the 480px width check and the "> 4 segments" condition in renderLong() to match when your layout actually needs to collapse.
  5. 5
    Wire real hrefsReplace the demo's href="#" placeholders with real ancestor URLs so each breadcrumb link actually navigates.

Real-world uses

Common Use Cases

Nested Content Hierarchies
Documentation sites, category trees, and CMS page hierarchies
File & Folder Navigation
File manager or file manager UI path bars showing the current folder depth
GEAR
Multi-Step Settings
Deep settings pages, paired with a sidebar navigation for the primary section switcher
CARD
E-Commerce Category Pages
Product category paths like Home / Electronics / Laptops / Gaming

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.