HTMLmediumnavigation
Implement accessible breadcrumb navigation
rule · breadcrumb-navigation
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
HTML
<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
TSX
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
TSX
<Breadcrumb
items={[
{ label: 'Products', href: '/products' },
{ label: 'Electronics', href: '/products/electronics' },
{ label: 'Laptops', href: '/products/electronics/laptops' },
]}
/>With Structured Data (JSON-LD)
TSX
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
TSX
'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
TSX
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
CSS
.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
HTML
<!-- 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