# Breadcrumb

Hierarchical trail with the current page marked via aria-current.

Preview: https://design.freecodecamp.org/playground#breadcrumb

## Add to your project

Use React and TypeScript. Required packages: `react@>=18 <20`. No freeCodeCamp package is needed.

Copy the files below to the indicated paths, relative to your project root. If you change the layout, update relative imports too.

Import the CSS once from your application entry. For an entry in src/:

```ts
import './ui/theme/tokens.css';
import './ui/theme/base.css';
import './ui/breadcrumb/breadcrumb.css';
```

The theme is shared: reuse it if already installed. Fonts use /fonts/ URLs on your host. Download the font files listed in https://design.freecodecamp.org/registry/starter.md or change those URLs in your copied tokens.css.

## Example

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

export function Example() {
  return (
    <Breadcrumb>
      <Breadcrumb.Item href='/'>Docs</Breadcrumb.Item>
      <Breadcrumb.Item href='/playground#navbar'>Navigation</Breadcrumb.Item>
      <Breadcrumb.Item active>Breadcrumb</Breadcrumb.Item>
    </Breadcrumb>
  );
}
```

## Interaction guidance

Review https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/ and test keyboard operation in your project.

## Component source

### src/ui/breadcrumb/Breadcrumb.tsx

Source: https://design.freecodecamp.org/registry/breadcrumb/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;
```

### src/ui/breadcrumb/breadcrumb.css

Source: https://design.freecodecamp.org/registry/breadcrumb/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;
}
```

## Shared source: Theme

### src/ui/theme/tokens.css

Source: https://design.freecodecamp.org/registry/theme/tokens.css

```css
@font-face {
  font-family: 'Lato';
  src: url('/fonts/Lato-Light.woff') format('woff');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Lato';
  src: url('/fonts/Lato-Regular.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Lato';
  src: url('/fonts/Lato-Italic.woff') format('woff');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: 'Lato';
  src: url('/fonts/Lato-Bold.woff') format('woff');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Lato';
  src: url('/fonts/Lato-BoldItalic.woff') format('woff');
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: 'Lato';
  src: url('/fonts/Lato-Black.woff') format('woff');
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Hack-ZeroSlash';
  src: url('/fonts/Hack-ZeroSlash-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Hack-ZeroSlash';
  src: url('/fonts/Hack-ZeroSlash-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Hack-ZeroSlash';
  src: url('/fonts/Hack-ZeroSlash-Italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: 'Hack-ZeroSlash';
  src: url('/fonts/Hack-ZeroSlash-BoldItalic.woff2') format('woff2');
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

:root {
  --gray-00: #ffffff;
  --gray-00-translucent: rgba(255, 255, 255, 0.85);
  --gray-05: #f5f6f7;
  --gray-10: #dfdfe2;
  --gray-15: #d0d0d5;
  --gray-45: #858591;
  --gray-75: #3b3b4f;
  --gray-80: #2a2a40;
  --gray-85: #1b1b32;
  --gray-90: #0a0a23;
  --gray-90-translucent: rgba(10, 10, 35, 0.85);

  --purple-light: #dbb8ff;
  --purple-mid: #9400d3;
  --purple-dark: #5a01a7;
  --yellow-light: #ffc300;
  --yellow-gold: #ffbf00;
  --yellow-style: #f1be32;
  --yellow-dark: #4d3800;
  --blue-light: #99c9ff;
  --blue-light-translucent: rgba(153, 201, 255, 0.3);
  --blue-mid: #198eee;
  --blue-dark: #002ead;
  --blue-dark-translucent: rgba(0, 46, 173, 0.3);
  --green-light: #acd157;
  --green-dark: #00471b;
  --red-light: #ffadad;
  --red-dark: #850000;
  --love-light: #f8577c;
  --love-dark: #f82153;
  --orange: #eda971;

  --editor-background-light: #fffffe;
  --editor-background-dark: #2a2b40;

  --syntax-keyword: #dbb8ff;
  --syntax-fn: #99c9ff;
  --syntax-string: #acd157;
  --syntax-class: #f1be32;
  --syntax-number: #f78c6c;
  --syntax-tag: #f07178;
  --syntax-operator: #89ddff;
  --syntax-invalid: #ff5370;
  --syntax-comment: #858591;
  --syntax-plain: #eeffff;

  --font-sans:
    'Lato', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-mono: 'Hack-ZeroSlash', 'Fira Mono', Menlo, Consolas, monospace;

  --fs-base: 18px;
  --fs-sm: 16px;
  --fs-md: 18px;
  --fs-lg: 24px;
  --fs-xl: 32px;
  --fs-2xl: 42px;
  --fs-3xl: 56px;
  --fs-display: clamp(2.5rem, 5vw, 3.75rem);

  --lh-tight: 1.2;
  --lh-snug: 1.33;
  --lh-base: 1.42857143;
  --lh-loose: 1.6;

  --fw-light: 300;
  --fw-regular: 400;
  --fw-bold: 700;
  --fw-black: 900;

  --space-0: 0;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  --border-width-hair: 1px;
  --border-width-default: 2px;
  --border-width-thick: 3px;
  --radius-none: 0;
  --radius-sm: 2px;

  --focus-outline-color: var(--blue-mid);
  --focus-outline-width: 3px;

  --z-breadcrumbs: 100;
  --z-flash: 150;
  --z-site-header: 200;
  --z-modal: 1050;

  --ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-fast: 120ms;
  --dur-base: 180ms;
  --dur-slow: 260ms;

  --header-height: 48px;
  --breadcrumbs-height: 32px;
  --sidebar-width: 288px;
  --content-max: 1040px;

  color-scheme: dark;
}

.dark-palette,
:root {
  color-scheme: dark;
  --foreground-primary: var(--gray-00);
  --foreground-secondary: var(--gray-05);
  --foreground-tertiary: var(--gray-10);
  --foreground-quaternary: var(--gray-15);
  --foreground-muted: #b0b0bd;

  --background-primary: var(--gray-90);
  --background-primary-translucent: var(--gray-90-translucent);
  --background-secondary: var(--gray-85);
  --background-tertiary: #33334f;
  --background-quaternary: #4b4b66;

  --highlight-color: var(--blue-light);
  --highlight-background: var(--blue-dark);
  --selection-color: var(--blue-light-translucent);

  --success-color: var(--green-light);
  --success-background: var(--green-dark);
  --danger-color: var(--red-light);
  --danger-background: var(--red-dark);
  --warning-color: var(--yellow-light);
  --warning-background: var(--yellow-dark);
  --purple-color: var(--purple-light);
  --purple-background: var(--purple-dark);
  --love-color: var(--love-light);

  --editor-background: var(--editor-background-dark);

  --cta-background: var(--yellow-gold);
  --cta-foreground: var(--gray-90);

  --surface-elevation-1: rgba(255, 255, 255, 0.045);
  --surface-elevation-2: rgba(255, 255, 255, 0.09);
  --border-soft: var(--background-tertiary);
  --border-strong: var(--background-quaternary);
}

.light-palette {
  --foreground-primary: var(--gray-90);
  --foreground-secondary: var(--gray-85);
  --foreground-tertiary: var(--gray-80);
  --foreground-quaternary: var(--gray-75);
  --foreground-muted: #5a5a68;

  --background-primary: var(--gray-00);
  --background-primary-translucent: var(--gray-00-translucent);
  --background-secondary: var(--gray-05);
  --background-tertiary: #c5c5cc;
  --background-quaternary: #a8a8b4;

  --highlight-color: var(--blue-dark);
  --highlight-background: var(--blue-light);
  --selection-color: var(--blue-dark-translucent);

  --success-color: var(--green-dark);
  --success-background: var(--green-light);
  --danger-color: var(--red-dark);
  --danger-background: var(--red-light);
  --warning-color: var(--yellow-dark);
  --warning-background: var(--yellow-light);
  --purple-color: var(--purple-dark);
  --purple-background: var(--purple-light);
  --love-color: var(--love-dark);

  --editor-background: var(--editor-background-light);

  --cta-background: var(--yellow-gold);
  --cta-foreground: var(--gray-90);

  --surface-elevation-1: rgba(10, 10, 35, 0.05);
  --surface-elevation-2: rgba(10, 10, 35, 0.09);
  --border-soft: var(--background-tertiary);
  --border-strong: var(--background-quaternary);

  color-scheme: light;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  font-size: var(--fs-md);
  font-family: var(--font-sans);
  line-height: var(--lh-base);
  color: var(--foreground-primary);
  background: var(--background-primary);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-height) + 24px);
}

body {
  margin: 0;
  font-family: var(--font-sans);
  color: var(--foreground-primary);
  background: var(--background-primary);
}

::selection {
  background: var(--selection-color);
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--font-sans);
  font-weight: var(--fw-bold);
  color: var(--foreground-primary);
  line-height: var(--lh-snug);
  margin: 0 0 12px 0;
}
h1 {
  font-size: var(--fs-3xl);
  line-height: var(--lh-tight);
  letter-spacing: -0.01em;
}
h2 {
  font-size: var(--fs-2xl);
  letter-spacing: -0.005em;
}
h3 {
  font-size: var(--fs-xl);
}
h4 {
  font-size: var(--fs-lg);
}
h5 {
  font-size: var(--fs-md);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
h6 {
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--foreground-muted);
  font-family: var(--font-mono);
}

p {
  margin: 0 0 12px 0;
}

a {
  color: var(--highlight-color);
  text-decoration: underline;
  text-underline-position: under;
  text-underline-offset: 0.1em;
}
a:hover {
  color: var(--foreground-primary);
}

code,
pre,
kbd,
samp {
  font-family: var(--font-mono);
  font-size: 16px;
}
code {
  background: var(--background-tertiary);
  color: var(--foreground-tertiary);
}
:not(pre) > code {
  border: 1px solid var(--background-quaternary);
  padding: 1px 4px;
  overflow-wrap: anywhere;
  word-break: break-word;
}
pre {
  background: var(--editor-background);
  color: var(--foreground-tertiary);
  padding: 14px 16px;
  font-size: 14px;
  line-height: var(--lh-base);
  max-width: 100%;
  overflow-x: auto;
  margin: 0;
}
pre code {
  display: block;
  width: max-content;
  min-width: 100%;
  background: transparent;
  border: 0;
  padding: 0;
}

:focus-visible {
  outline: var(--focus-outline-width) solid var(--focus-outline-color);
  outline-offset: 0;
}

hr {
  border: 0;
  border-top: 1px solid var(--background-quaternary);
  margin: 24px 0;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
::-webkit-scrollbar-track {
  background: var(--background-primary);
}
::-webkit-scrollbar-thumb {
  background: var(--background-quaternary);
  border: 2px solid var(--background-primary);
}
::-webkit-scrollbar-thumb:hover {
  background: var(--foreground-muted);
}
```

### src/ui/theme/base.css

Source: https://design.freecodecamp.org/registry/theme/base.css

```css
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
```

## Adapting this component

Keep the component's semantics and keyboard behavior. Use the CSS variables to change its appearance. Check the result in your project; copied source does not receive automatic updates.

Source revision: 1914914 (2026-09-08). Component source: BSD-3-Clause. Preserve the license notice: https://design.freecodecamp.org/license.txt.

Design rules: https://design.freecodecamp.org/handbook.md

