# Breadcrumb

> Hierarchical trail of links with the current page marked via aria-current. Compound API plus a CSS pseudo-element separator so the visible slash never reaches the accessibility tree.

- Category: navigation
- Status: stable (since 1.0.0)
- A11y pattern: https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/
- Tokens: --foreground-primary, --foreground-secondary, --foreground-muted, --foreground-quaternary
- Playground: https://design.freecodecamp.org/playground#breadcrumb
- npm dependencies: `react@>=18 <20`
- Registry dependencies: [theme](https://design.freecodecamp.org/registry/theme.md)
- Files:
  - `Breadcrumb.tsx` → `src/ui/breadcrumb/Breadcrumb.tsx` (raw: https://design.freecodecamp.org/registry/breadcrumb/Breadcrumb.tsx)
  - `breadcrumb.css` → `src/ui/breadcrumb/breadcrumb.css` (raw: https://design.freecodecamp.org/registry/breadcrumb/breadcrumb.css)

## Install (copy source)

1. Ensure the theme is installed once per project - tokens.css + base.css imported globally, fonts available. See https://design.freecodecamp.org/registry/theme.md and https://design.freecodecamp.org/registry/starter.md.
2. Copy the files below into `src/ui/breadcrumb/` (adjust to your project layout) and import the CSS once from your global stylesheet, e.g. `@import './ui/breadcrumb/breadcrumb.css';`.
3. Colors, spacing and type come from tokens - tailor the component by editing the copied source; recolour by editing tokens.css, not the component CSS.

## Usage

Breadcrumb renders the path-to-here trail above page content. The
component is a labelled `<nav>` wrapping an ordered list; each item
lands as either a link or - for the active leaf - a non-link span
with `aria-current="page"`. The visible separator is a CSS
pseudo-element that screen readers ignore, so the announced
sequence stays "Home, Components, Breadcrumb" rather than the
literal "Home / Components / Breadcrumb" mush.

`href` is scheme-allowlisted (`https?:`, `/`, `#`, `mailto:`,
`tel:`); anything else falls through to a non-link span with a
`console.warn`. Mitigates an XSS surface where untrusted user input
could otherwise reach an `<a href>`.

## Accessibility

Renders as `<nav aria-label="Breadcrumb"><ol><li>…</li></ol></nav>`.
The active item drops the `<a>` and emits a span with
`aria-current="page"`. The decorative separator is a CSS
`::after` pseudo-element on every non-last item - invisible to
assistive tech, no extra DOM noise.

## Example

```tsx
import { Breadcrumb } from './ui/breadcrumb/Breadcrumb';

<Breadcrumb>
  <Breadcrumb.Item href="/">Docs</Breadcrumb.Item>
  <Breadcrumb.Item href="/#navigation">Navigation</Breadcrumb.Item>
  <Breadcrumb.Item active>Breadcrumb</Breadcrumb.Item>
</Breadcrumb>
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `aria-label` | `string` | no | - |  |
| `className` | `string` | no | - |  |

## Source: Breadcrumb.tsx

```tsx
import {
  createContext,
  useContext,
  Children,
  cloneElement,
  isValidElement,
  type ReactNode,
  type ReactElement,
  type AnchorHTMLAttributes
} from 'react';

export interface BreadcrumbProps {
  children: ReactNode;
  'aria-label'?: string;
  className?: string;
}

export interface BreadcrumbItemProps extends Omit<
  AnchorHTMLAttributes<HTMLAnchorElement>,
  'children'
> {
  children: ReactNode;
  active?: boolean;
}

interface BreadcrumbContextValue {
  total: number;
  index: number;
}

const BreadcrumbContext = createContext<BreadcrumbContextValue>({
  total: 0,
  index: 0
});

const SAFE_SCHEMES = /^(https?:|\/|#|mailto:|tel:)/i;

function safeHref(href: string | undefined): string | undefined {
  if (!href) return undefined;
  if (!SAFE_SCHEMES.test(href)) {
    if (typeof console !== 'undefined') {
      console.warn(`[Breadcrumb] rejected href: ${href}`);
    }
    return undefined;
  }
  return href;
}

function BreadcrumbRoot({
  children,
  className,
  'aria-label': ariaLabel = 'Breadcrumb'
}: BreadcrumbProps): ReactElement {
  const items = Children.toArray(children).filter(isValidElement);
  return (
    <nav
      aria-label={ariaLabel}
      className={['breadcrumb', className].filter(Boolean).join(' ')}
    >
      <ol className='breadcrumb__list'>
        {items.map((child, index) => (
          <BreadcrumbContext.Provider
            key={index}
            value={{ total: items.length, index }}
          >
            {cloneElement(child as ReactElement)}
          </BreadcrumbContext.Provider>
        ))}
      </ol>
    </nav>
  );
}

function BreadcrumbItem({
  active,
  href,
  children,
  className,
  ...rest
}: BreadcrumbItemProps): ReactElement {
  const { total, index } = useContext(BreadcrumbContext);
  const isLast = total > 0 && index === total - 1;
  const safe = safeHref(href);
  const isActive = active === true || (active === undefined && isLast && !safe);
  const liClass = ['breadcrumb__item', className].filter(Boolean).join(' ');

  if (isActive || !safe) {
    return (
      <li className={liClass}>
        <span
          aria-current={isActive ? 'page' : undefined}
          className='breadcrumb__current'
        >
          {children}
        </span>
      </li>
    );
  }

  return (
    <li className={liClass}>
      <a href={safe} className='breadcrumb__link' {...rest}>
        {children}
      </a>
    </li>
  );
}

export const Breadcrumb = Object.assign(BreadcrumbRoot, {
  Item: BreadcrumbItem
});

export default Breadcrumb;
```

## Source: breadcrumb.css

```css
.breadcrumb {
  font-size: var(--fs-sm);
}
.breadcrumb__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2, 8px);
  align-items: center;
}
.breadcrumb__item {
  display: inline-flex;
  align-items: center;
}
.breadcrumb__item:not(:last-child)::after {
  content: var(--breadcrumb-separator-content, '/');
  color: var(--foreground-muted, var(--foreground-secondary));
  padding-inline-start: var(--space-2, 8px);
}
.breadcrumb__link {
  color: var(--foreground-secondary);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.breadcrumb__link:hover,
.breadcrumb__link:focus-visible {
  color: var(--foreground-primary);
  border-bottom-color: var(--foreground-quaternary);
}
.breadcrumb__current {
  color: var(--foreground-primary);
  font-weight: 600;
}
```

## HTML / vanilla variant

```html
<nav aria-label="Breadcrumb" class="breadcrumb">
  <ol class="breadcrumb__list">
    <li class="breadcrumb__item">
      <a class="breadcrumb__link" href="/">Docs</a>
    </li>
    <li class="breadcrumb__item">
      <a class="breadcrumb__link" href="/#navigation">Navigation</a>
    </li>
    <li class="breadcrumb__item">
      <span class="breadcrumb__current" aria-current="page">Breadcrumb</span>
    </li>
  </ol>
</nav>
```

Interactive behaviours for plain HTML come from the vanilla runtime (data-uikit-* attributes): https://design.freecodecamp.org/registry/vanilla.md - or download https://design.freecodecamp.org/cdn/uikit.global.js once and self-host it (do not hotlink).

## For coding agents

This library is distributed as copyable source, not an npm package. Start at https://design.freecodecamp.org/registry/starter.md, discover components via https://design.freecodecamp.org/llms.txt, and copy files into the consuming project. Keep token names intact; recolour by editing the copied tokens.css.
