Breadcrumbs
A breadcrumb trail with the correct markup and matching structured data.
<nav class="crumbs" aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/shirts/">Shirts</a></li>
<li><a href="/shirts/linen/">Linen</a></li>
<li><span aria-current="page">Ellery shirt, oatmeal</span></li>
</ol>
</nav>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com/" },
{ "@type": "ListItem", "position": 2, "name": "Shirts", "item": "https://example.com/shirts/" },
{ "@type": "ListItem", "position": 3, "name": "Linen", "item": "https://example.com/shirts/linen/" },
{ "@type": "ListItem", "position": 4, "name": "Ellery shirt, oatmeal" }
]
}
</script>body { font: 16px/1.6 system-ui, -apple-system, sans-serif; padding: 24px; }
.crumbs ol {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px;
list-style: none;
margin: 0;
padding: 0;
font-size: .86rem;
}
.crumbs li { display: flex; align-items: center; }
/* the separator is decoration, so it belongs in CSS, not in the markup */
.crumbs li + li::before {
content: "/";
margin: 0 8px;
color: #b8c2d2;
}
.crumbs a {
color: #5b6b83;
text-decoration: none;
padding: 2px 0;
}
.crumbs a:hover { color: #3b4fe4; text-decoration: underline; text-underline-offset: 3px; }
.crumbs [aria-current="page"] { color: #16202e; font-weight: 500; }
/* on a narrow screen, collapse the middle and keep the ends */
@media (max-width: 520px) {
.crumbs li:not(:first-child):not(:last-child):not(:nth-last-child(2)) { display: none; }
.crumbs li:nth-last-child(2)::before { content: "…/"; }
}How it works
Small component, disproportionate SEO value: the trail replaces the bare URL under your title in Google results, and it is one of the few rich results that still shows up reliably.
An ordered list, because the order means something. A breadcrumb is a hierarchy, so <ol> is correct and <ul> is not. Screen readers announce the position and the total, which is exactly the information the trail conveys.
The separator lives in CSS. Putting a literal / in the markup means screen readers read "slash" between every item. A ::before pseudo-element is decorative and is skipped.
The last item is not a link. You are already on that page, so a link to it is a dead end. Mark it with aria-current="page" instead.
The JSON-LD mirrors the visible trail. It has to — structured data must describe what is actually on the page. Note that the final item has a name but no item URL, which is what Google's specification asks for on the current page.
Accessibility notes
aria-label="Breadcrumb" on the <nav> is what distinguishes this landmark from the main navigation. Without it, a screen reader user cycling through landmarks hears "navigation" twice with no way to tell them apart.
Keep the text at a readable size. Breadcrumbs are often set at 12 pixels, which is below what many people can comfortably read, and they are functional navigation rather than fine print.
Making it yours
The mobile rule collapses the middle of long trails to an ellipsis while keeping the first and last two items, which is usually the right compromise. For a three-level trail it changes nothing.
The schema generator will build the JSON-LD from your actual URLs, including escaping, so you do not have to hand-edit the block.
Related templates
Responsive navbar
A header that collapses to a hamburger under 820px. No framework, keyboard accessible.
HTMLCSSJavaScriptTailwindAccessible dropdown menu
A nav dropdown that opens on hover, click and keyboard, with arrow-key support.
HTMLCSSJavaScriptCollapsible sidebar
A dashboard sidebar that collapses to icons and becomes a drawer on mobile.
HTMLCSSJavaScriptSEO-ready HTML boilerplate
A full head block with meta, Open Graph, Twitter cards and structured data.
HTMLCSSCheck your version
Once you have edited this, the HTML validator will catch any tag you left unclosed, and the formatter will tidy the indentation. Both run in your browser.