Most demos need a newer browser

They use :has() and container queries. Update your browser, or on an iPhone update iOS to 16.4 or later.

CSS Breadcrumb Navigation

Navigation breadcrumbs with ::before separators using CSS content. Three separator styles (slash, arrow, caret) and a pill variant.

Level
Beginner
Published
Updated
Reading time
8 min

Try it

3 live demos

Four separator styles, one set of markup

Other ways to build it

Build it

6 snippets to copy

CSS Breadcrumb Navigation

HTML

<nav aria-label="Breadcrumb">
  <ol>
    <li><a href="#">Home</a></li>
    <li><a href="#">Components</a></li>
    <li><a href="#">Navigation</a></li>
    <li>Breadcrumb</li>
  </ol>
</nav>

<!-- Same markup, different separator -->
<nav aria-label="Breadcrumb" class="bc-arrow">
  <ol>
    <li><a href="#">Dashboard</a></li>
    <li><a href="#">Settings</a></li>
    <li>Profile</li>
  </ol>
</nav>

<nav aria-label="Breadcrumb" class="bc-caret">
  <ol>
    <li><a href="#">Products</a></li>
    <li><a href="#">Electronics</a></li>
    <li>Laptops</li>
  </ol>
</nav>

<nav aria-label="Breadcrumb" class="bc-pill">
  <ol>
    <li><a href="#">Blog</a></li>
    <li><a href="#">CSS Tips</a></li>
    <li>Breadcrumbs</li>
  </ol>
</nav>

CSS

nav ol {
  list-style: none;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0;
  margin: 0 0 1.5rem;
  padding: 0;
}

nav li {
  display: flex;
  align-items: center;
  font-size: .9rem;
}

/* The separator sits on every item that follows another one,
   muted below the crumbs so the labels lead */
nav li + li::before {
  content: "/";
  color: #88888f;
  opacity: .3;
  margin: 0 .5rem;
}

nav li:last-child {
  color: #f0f0f0;
  font-weight: 500;
}

nav a {
  color: #88888f;
  text-decoration: none;
  transition: color .15s;
}

nav a:hover {
  color: #b8ff57;
}

/* Arrow variant */
.bc-arrow li + li::before {
  content: "›";
  font-size: 1.1rem;
  line-height: 1;
}

/* Caret drawn from borders instead of a character:
   sized in em, so it scales with the text */
.bc-caret li + li::before {
  content: "";
  width: 0;
  height: 0;
  border-top: .3em solid transparent;
  border-bottom: .3em solid transparent;
  border-left: .38em solid currentColor;
  margin: 0 .65rem;
}

/* Pill variant drops the separator and spaces the items instead */
.bc-pill li {
  background: #1c1c1e;
  padding: .25rem .75rem;
  border-radius: 99px;
  font-size: .8rem;
}

.bc-pill li + li::before {
  display: none;
}

.bc-pill li + li {
  margin-left: .35rem;
}

.bc-pill li:last-child {
  background: rgba(184, 255, 87, .1);
  color: #b8ff57;
}

Interlocking arrows drawn from borders

Each crumb has a point on its right and a notch cut into its left, and neither is an image or a glyph. A zero-size box with a 15px transparent border and one side colored paints a solid triangle, which is how CSS drew shapes for years before clip-path. The right hand gap between crumbs is exactly the width of the triangle, so the point fills it and the chain reads as continuous without any of the pieces overlapping or needing a stacking order.

HTML

<nav class="bc-way" aria-label="Breadcrumb">
  <ol>
    <li><a href="#">Home</a></li>
    <li><a href="#">Store</a></li>
    <li><a href="#">Audio</a></li>
    <li aria-current="page">Headphones</li>
  </ol>
</nav>

CSS

/* the crumb height and the border width have to match:
   a 15px border makes a 30px triangle */
.bc-way li {
  position: relative;
  display: flex;
  align-items: center;
  height: 30px;
  margin-right: 15px;
  padding: 0 .7rem 0 1.4rem;
  background: #1c1c1e;
  font-size: .8rem;
}

/* the point, sitting in the gap to the right */
.bc-way li::after {
  content: "";
  position: absolute;
  left: 100%;
  top: 0;
  border: 15px solid transparent;
  border-left-color: #1c1c1e;
}

/* the notch, cut in the page background color */
.bc-way li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  border: 15px solid transparent;
  border-left-color: #141415;
  /* the notch is not a separator, so it drops the separator's fade */
  opacity: 1;
}

.bc-way li:first-child::before { display: none; }
.bc-way li:last-child::after  { display: none; }

.bc-way li:last-child {
  margin-right: 0;
  background: rgba(184, 255, 87, .1);
  color: #b8ff57;
}

A deep trail that scrolls instead of wrapping

Six levels of hierarchy will not fit across a phone, and wrapping to three lines pushes the page content down. The alternative is one line with flex-wrap: nowrap and overflow-x: auto. On its own that opens showing the site root, which is the least useful end. direction: rtl on the scroll container flips where the scroll starts, flex-direction: row-reverse on the list puts the crumbs back into reading order, and direction: ltr on each item keeps the labels and separators from mirroring. The trail opens on the current page and scrolls back towards Home. Drag it sideways to see the rest.

HTML

<div class="bc-shelf">
  <div class="bc-scroll">
    <nav aria-label="Breadcrumb">
      <ol>
        <li><a href="#">Home</a></li>
        <li><a href="#">Documentation</a></li>
        <!-- ...four more levels... -->
        <li aria-current="page">Separators</li>
      </ol>
    </nav>
  </div>
  <span class="bc-edge"></span>
</div>

CSS

.bc-shelf {
  position: relative;
  max-width: 320px;
}

/* rtl on the scroller starts it at the far end, which is
   where the current page sits */
.bc-scroll {
  overflow-x: auto;
  direction: rtl;
}

/* row-reverse inside an rtl container runs left to right
   again, so the crumbs keep reading order while the scroll
   origin stays flipped; max-content stops the list
   shrinking to the width of the scroller */
.bc-scroll ol {
  direction: rtl;
  flex-direction: row-reverse;
  flex-wrap: nowrap;
  white-space: nowrap;
  width: max-content;
}

/* back to ltr per item, or labels and separators mirror */
.bc-scroll li {
  direction: ltr;
  flex: 0 0 auto;
}

/* a fade at the start, so the trail reads as cut off
   rather than as beginning at Documentation */
.bc-edge {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 36px;
  pointer-events: none;
  background: linear-gradient(to right, #141415, transparent);
}

Understand it

How it works and 6 properties

A breadcrumb trail is an ordered list of the pages between the site root and where the reader is now. The markup is a <nav> holding an <ol>, and the separators between items are not in the markup at all. li + li::before selects every list item that follows another one and generates the character in front of it, which keeps the HTML free of slashes.

The four trails in the demo above share identical markup and differ only in the content value and a few pixels of spacing. Two more versions follow: an interlocking arrow style built from CSS border triangles, and a trail that scrolls sideways when the path is deeper than the space available.

How it works

Breadcrumbs are an <ol> list styled as a horizontal trail. The separator between items is injected using li + li::before, where the adjacent sibling combinator selects every <li> that immediately follows another. The content property can be any character, string, or even an SVG. No JavaScript or extra HTML needed for the separators.

The choice of li + li::before over li::after matters. Using ::after on every item puts a separator after the last one too, which then has to be removed with a second rule. Using ::before on items that follow another item produces exactly one separator between each pair and none at either end, from a single rule. + is the adjacent sibling combinator, so it only matches an item directly after another, which is what makes it precise here rather than the looser ~.

Generated content is not text in the document. It cannot be selected and is not copied when the reader copies the trail.

The caret in the third trail is drawn rather than typed. A zero size box with one colored border between two transparent ones gives a solid triangle that scales with the font size and needs no glyph, which sidesteps the fact that characters like the guillemet render at different weights across fonts. The arrow version below uses the same trick with a 15px border. That trick is how CSS drew shapes long before clip-path existed.

Only the separator is styling. The trail's meaning belongs in the markup: <nav aria-label="Breadcrumb"> so the region can be found and skipped, an <ol> because the order carries information, and aria-current="page" on the last item. Pair that with BreadcrumbList structured data, which is what search engines read to show a path instead of a raw URL under a result. The visual trail and the structured data are separate declarations of the same fact, and they should be generated from the same source so they cannot drift apart.

A breadcrumb answers a different question from a pagination bar or from a tab strip. Those two say which of several equivalent views is showing. A breadcrumb says where this page sits in a hierarchy, so it is a path rather than a selection, and the last item is a label rather than a control.

Accessibility notes

Wrap the list in <nav aria-label="Breadcrumb">. Without a name, the nav appears among other navigation regions with nothing to distinguish it. Use an <ol> rather than a <ul>, because the sequence is the information. Mark the final item with aria-current="page" so a reader knows the trail has ended at the page they are on rather than at another link.

Leave the separators as generated content and do not put them in the HTML. Browsers disagree about whether generated content reaches the accessibility tree, and a slash between every crumb is noise in either case. If a separator carries meaning, it does not belong in content.

The last crumb is usually not a link, since linking to the current page gives a reader nothing. Style it as text with enough contrast to read as the endpoint of the trail rather than as a disabled link. Where it must stay a link for consistency, keep aria-current="page" on it so the state is marked explicitly instead of inferred from color.

A trail that scrolls sideways still has to be reachable by keyboard. The links inside it are focusable, and a browser scrolls a focused element into view automatically, so tabbing through works. Do not set tabindex="-1" on the scroll container to tidy it up, or the reader loses the ability to scroll it without a pointer.

Mistakes worth avoiding

  • Writing the separators into the HTML. They become selectable text and are copied along with the crumb labels.
  • Using li::after for the separator and forgetting the :last-child rule that removes the trailing one. The trail ends in a dangling slash, which reads as a truncated path.
  • Letting a deep trail wrap onto three lines on a phone. Either allow one wrap and truncate the middle crumbs, or switch to a single line that scrolls sideways.
  • Making the last crumb a link to the page the reader is already on. It is a dead control, and without aria-current there is nothing to say the trail has ended.
  • Shipping the visual trail without BreadcrumbList structured data. The hierarchy is only in the CSS at that point, and search results show the raw URL instead of the path.

Ship it

4 features in 4 browsers, 5 questions

Browser support

Feature Chrome Firefox Safari Edge
content423.112
+ combinator423.112
flexbox21286.112
logical properties89661589

Figures come from Can I Use for CSS generated content, CSS 2.1 selectors, the Flexible Box Layout module, and CSS logical properties. Everything the separators need has been supported for well over a decade. The logical properties row applies only if the trail has to mirror in right to left languages, where margin-inline-start and padding-inline flip with the writing mode and their physical equivalents do not. direction: rtl on a scroll container has no entry in the data this table checks against. It is part of CSS 2.1, and it has behaved consistently for as long as overflow has.

What you can build with it

  • Documentation hierarchies. Section, page and heading, so a reader arriving from a search result can see how deep in the tree they landed.
  • Ecommerce category paths. Department to category to product. This is the case where BreadcrumbList markup pays for itself, because search results show the path instead of the URL.
  • Multi step checkout. A read-only trail of the steps already completed, sitting alongside a multi step form rather than replacing its controls.
  • File and folder browsers. A deep path that needs the scrolling variant, since the segment the reader cares about is the last one.
  • Admin record pages. Account to project to record, where the crumbs are the fastest route back up one level without hunting in a sidebar.

Frequently asked questions

How do I add separators between breadcrumbs in CSS?
Use li + li::before { content: "/" }. The adjacent sibling combinator matches every item that follows another, so each pair gets one separator and neither end gets a stray one. Nothing goes in the HTML.
Should breadcrumbs be a ul or an ol?
An <ol>. The order of a breadcrumb is the meaning of it, and an ordered list is what says so. Wrap it in <nav aria-label="Breadcrumb"> so the region has a name.
Do breadcrumbs help SEO?
The markup does. Adding BreadcrumbList structured data lets a search result show the site path in place of the URL, which describes where the page sits. The visual trail on its own is a usability feature, and the two should be generated from the same data so they cannot disagree.
How do I handle a breadcrumb that is too long for mobile?
Two options. Keep it on one line with flex-wrap: nowrap and overflow-x: auto, and start the container scrolled to the end so the current page is what shows. Or collapse the middle crumbs to an ellipsis and keep the first and last, which is shorter but hides the path.
Can I use an SVG as a breadcrumb separator?
Yes, an SVG works as a breadcrumb separator. content: url(chevron.svg) works, as does a data URI, and background-image on the pseudo-element is the other route. Both cost a paint of an external resource where a drawn caret costs nothing, so the border triangle is usually the better trade.