Implement accessible breadcrumb navigation
Breadcrumb navigation is implemented with proper semantic markup and ARIA attributes for accessibility.
- Use nav element with aria-label='Breadcrumb'
- Use ordered list (ol) for semantic structure
- Mark current page with aria-current='page'
- Keep any structured data aligned with the visible breadcrumb trail
Rule Details
Accessible breadcrumbs help users understand their location within the site hierarchy. The first breadcrumb does not have to be Home; include a homepage item only when it is part of the visible breadcrumb trail and site hierarchy.
Code Example
<nav aria-label="Breadcrumb" class="breadcrumb">
<ol>
<li>
<a href="/products">Products</a>
</li>
<li>
<a href="/products/electronics">Electronics</a>
</li>
<li>
<a href="/products/electronics/laptops" aria-current="page">Laptops</a>
</li>
</ol>
</nav>Why It Matters
Proper breadcrumb markup helps screen reader users understand site hierarchy and their current location, while also improving SEO when structured data accurately mirrors the visible trail.
Accessibility Requirements
| Requirement | Implementation |
|---|---|
| Container | <nav> with aria-label="Breadcrumb" |
| List structure | Ordered list (<ol>) |
| Current page | aria-current="page" |
| Separators | CSS or aria-hidden text |
| Home item | Optional; include only when it is visible and meaningful |
SEO Requirements
- Keep visible breadcrumbs and
BreadcrumbListJSON-LD in the same order with the same labels. - Do not emit
BreadcrumbListschema for a homepage, landing page, or hub page with only one meaningful breadcrumb item. - Use global navigation, the site logo, or a separate home control for homepage access when
Homeis not part of the breadcrumb trail.
React Breadcrumb Component
import { useId } from 'react'
interface BreadcrumbItem {
label: string
href: string
}
interface BreadcrumbProps {
items: BreadcrumbItem[]
}
export function Breadcrumb({ items }: BreadcrumbProps) {
if (items.length === 0) return null
return (
<nav aria-label="Breadcrumb" className="breadcrumb">
<ol className="breadcrumb__list">
{items.map((item, index) => {
const isLast = index === items.length - 1
return (
<li key={item.href} className="breadcrumb__item">
{isLast ? (
<span aria-current="page" className="breadcrumb__current">
{item.label}
</span>
) : (
<a href={item.href} className="breadcrumb__link">
{item.label}
</a>
)}
</li>
)
})}
</ol>
</nav>
)
}Usage
<Breadcrumb
items={[
{ label: 'Products', href: '/products' },
{ label: 'Electronics', href: '/products/electronics' },
{ label: 'Laptops', href: '/products/electronics/laptops' },
]}
/>With Structured Data (JSON-LD)
interface BreadcrumbItem {
label: string
href: string
}
interface BreadcrumbProps {
items: BreadcrumbItem[]
baseUrl?: string
}
export function BreadcrumbWithSchema({ items, baseUrl = '' }: BreadcrumbProps) {
const schema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.label,
item: `${baseUrl}${item.href}`,
})),
}
return (
<>
<nav aria-label="Breadcrumb" className="breadcrumb">
<ol className="breadcrumb__list">
{items.map((item, index) => {
const isLast = index === items.length - 1
return (
<li key={item.href} className="breadcrumb__item">
{isLast ? (
<span aria-current="page">{item.label}</span>
) : (
<a href={item.href}>{item.label}</a>
)}
</li>
)
})}
</ol>
</nav>
{/* Structured data for SEO; keep this aligned with the visible trail. */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
</>
)
}Next.js Dynamic Breadcrumbs
'use client'
import { usePathname } from 'next/navigation'
interface PathConfig {
[key: string]: string
}
const pathLabels: PathConfig = {
products: 'Products',
electronics: 'Electronics',
laptops: 'Laptops',
accessories: 'Accessories',
}
export function AutoBreadcrumb() {
const pathname = usePathname()
const segments = pathname.split('/').filter(Boolean)
const items = segments.map((segment, index) => ({
label: pathLabels[segment] || segment.replace(/-/g, ' '),
href: '/' + segments.slice(0, index + 1).join('/'),
}))
// Add a Home item here only if the visible breadcrumb trail includes it.
return <Breadcrumb items={items} />
}With Icons
interface BreadcrumbProps {
items: BreadcrumbItem[]
leadingIcon?: React.ReactNode
separator?: React.ReactNode
}
export function Breadcrumb({
items,
leadingIcon,
separator = '/'
}: BreadcrumbProps) {
return (
<nav aria-label="Breadcrumb" className="breadcrumb">
<ol className="breadcrumb__list">
{items.map((item, index) => {
const isFirst = index === 0
const isLast = index === items.length - 1
return (
<li key={item.href} className="breadcrumb__item">
{index > 0 && (
<span aria-hidden="true" className="breadcrumb__separator">
{separator}
</span>
)}
{isLast ? (
<span aria-current="page" className="breadcrumb__current">
{item.label}
</span>
) : (
<a href={item.href} className="breadcrumb__link">
{isFirst && leadingIcon && (
<span aria-hidden="true" className="breadcrumb__icon">
{leadingIcon}
</span>
)}
{isFirst && leadingIcon ? (
<span className="sr-only">{item.label}</span>
) : (
item.label
)}
</a>
)}
</li>
)
})}
</ol>
</nav>
)
}Styling
.breadcrumb__list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
list-style: none;
margin: 0;
padding: 0;
font-size: 0.875rem;
}
.breadcrumb__item {
display: flex;
align-items: center;
gap: 0.5rem;
}
/* CSS-based separators (alternative to inline) */
.breadcrumb__item:not(:first-child)::before {
content: '/';
color: #999;
}
.breadcrumb__link {
color: #0066cc;
text-decoration: none;
}
.breadcrumb__link:hover {
text-decoration: underline;
}
.breadcrumb__link:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
border-radius: 2px;
}
.breadcrumb__current {
color: #666;
font-weight: 500;
}
.breadcrumb__separator {
color: #999;
}
.breadcrumb__icon {
display: inline-flex;
margin-right: 0.25rem;
}
/* Screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
/* Responsive: hide middle items on small screens */
@media (max-width: 640px) {
.breadcrumb__list {
flex-wrap: nowrap;
overflow: hidden;
}
/* Show first, last, and ellipsis */
.breadcrumb__item:not(:first-child):not(:last-child) {
display: none;
}
.breadcrumb__item:first-child + .breadcrumb__item:not(:last-child)::before {
content: '...';
}
}Common Patterns
<!-- Don't link the current page -->
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/products">Products</a></li>
<li><span aria-current="page">Laptops</span></li>
</ol>
</nav>
<!-- With separators hidden from screen readers -->
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/products">Products</a></li>
<li aria-hidden="true">/</li>
<li><span aria-current="page">Laptops</span></li>
</ol>
</nav>Verification
- Navigate with screen reader (announces "Breadcrumb navigation")
- Verify ordered list structure
- Confirm current page is announced
- Check all links are keyboard focusable
- Verify separators aren't announced
- Verify any
BreadcrumbListJSON-LD matches the visible breadcrumb items - Avoid one-item breadcrumb schema on flat, landing, or hub pages
- Test structured data with Rich Results Test
- Check responsive behavior on small screens
The current page in breadcrumbs should not be a link—it should be plain text with aria-current="page". Linking to the current page creates confusion.
Use with AI
Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.
Check
Verify implementation
Verify breadcrumbs use nav element with aria-label, ordered list markup, and aria-current for the current page.
Fix
Auto-fix issues
Implement breadcrumbs using semantic nav, ol/li structure, and proper ARIA attributes including aria-current='page'.
Explain
Learn more
Explain how properly marked up breadcrumbs improve navigation, SEO, and screen reader accessibility.
Review
Code review
Review templates, server-rendered HTML, and shared components that output markup related to Implement accessible breadcrumb navigation. Flag exact elements, attributes, and routes where the rendered HTML violates the rule.