Navigation

Breadcrumb

Shows where the current page sits in a hierarchy, with a link to each level above it.

Built on
<nav><ol>aria-currentCSS generated content with alt text
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
1.4 kB
Dependencies
React
Code
import { Breadcrumb } from "@/components/equip/breadcrumb";

export default function Demo() {
  return (
    <Breadcrumb
      style={{ width: 216 }}
      items={[
        { label: "Acme", href: "#acme" },
        { label: "larder-storefront", href: "#larder-storefront" },
        { label: "Deployments" },
      ]}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<nav style="width:216px" aria-label="Breadcrumb" class="eq-breadcrumb">
  <ol class="eq-breadcrumb-list">
    <li class="eq-breadcrumb-item">
      <a class="eq-breadcrumb-link" href="#acme" title="Acme">Acme</a>
    </li>
    <li class="eq-breadcrumb-item">
      <a class="eq-breadcrumb-link" href="#larder-storefront" title="larder-storefront">larder-storefront</a>
    </li>
    <li class="eq-breadcrumb-item">
      <span class="eq-breadcrumb-text" title="Deployments" aria-current="page">Deployments</span>
    </li>
  </ol>
</nav>

Examples

File path

The last crumb keeps its href here, so it stays a link marked as the current page.

Code
import { Breadcrumb } from "@/components/equip/breadcrumb";

export default function Example() {
  return (
    <Breadcrumb
      aria-label="File path"
      items={[
        { label: "equip", href: "#equip" },
        { label: "registry", href: "#registry" },
        { label: "breadcrumb", href: "#registry-breadcrumb" },
        { label: "breadcrumb.css", href: "#breadcrumb-css" },
      ]}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<nav aria-label="File path" class="eq-breadcrumb">
  <ol class="eq-breadcrumb-list">
    <li class="eq-breadcrumb-item">
      <a class="eq-breadcrumb-link" href="#equip" title="equip">equip</a>
    </li>
    <li class="eq-breadcrumb-item">
      <a class="eq-breadcrumb-link" href="#registry" title="registry">registry</a>
    </li>
    <li class="eq-breadcrumb-item">
      <a class="eq-breadcrumb-link" href="#registry-breadcrumb" title="breadcrumb">breadcrumb</a>
    </li>
    <li class="eq-breadcrumb-item">
      <a class="eq-breadcrumb-link" href="#breadcrumb-css" title="breadcrumb.css" aria-current="page">breadcrumb.css</a>
    </li>
  </ol>
</nav>

Long middle crumbs

Middle crumbs are capped at 16 characters and shrink first when space runs out; the full name stays in the link text and its tooltip.

Code
import { Breadcrumb } from "@/components/equip/breadcrumb";

export default function Example() {
  return (
    <Breadcrumb
      style={{ maxWidth: "min(320px, 100%)" }}
      items={[
        { label: "Billing", href: "#billing" },
        { label: "Harbour Traders International Ltd", href: "#larder" },
        { label: "Invoices 2026 financial year", href: "#invoices" },
        { label: "INV-2041" },
      ]}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<nav style="max-width:min(320px, 100%)" aria-label="Breadcrumb" class="eq-breadcrumb">
  <ol class="eq-breadcrumb-list">
    <li class="eq-breadcrumb-item">
      <a class="eq-breadcrumb-link" href="#billing" title="Billing">Billing</a>
    </li>
    <li class="eq-breadcrumb-item">
      <a class="eq-breadcrumb-link" href="#larder" title="Harbour Traders International Ltd">Harbour Traders International Ltd</a>
    </li>
    <li class="eq-breadcrumb-item">
      <a class="eq-breadcrumb-link" href="#invoices" title="Invoices 2026 financial year">Invoices 2026 financial year</a>
    </li>
    <li class="eq-breadcrumb-item">
      <span class="eq-breadcrumb-text" title="INV-2041" aria-current="page">INV-2041</span>
    </li>
  </ol>
</nav>

Props

PropTypeDefaultWhat it does
items{ label: ReactNode; href?: string }[]noneFrom the root down to the current page. The last item gets aria-current="page" and may leave out href.
aria-labelstring"Breadcrumb"Names the landmark. Change it when a page has more than one trail.
...restComponentProps<"nav">noneEvery native nav attribute.

Anatomy

root.eq-breadcrumbA nav landmark.
list.eq-breadcrumb-listAn ordered list, so the depth is announced.
item.eq-breadcrumb-itemDraws the separator before itself.
link.eq-breadcrumb-link
text.eq-breadcrumb-textUsed instead of a link when an item has no href.

Keyboard

EnterFollows the focused link.

Notes

  • Renders on the server with no client JavaScript.
  • The separators are CSS with empty alt text, so screen readers hear only the crumbs.
  • Truncation is visual only; the accessible name of a cut-short crumb is still its full text.
  • The last item is marked with aria-current="page", as a link if you give it an href and as plain text if not.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/breadcrumb.tsx45 lines
import type { ComponentProps, ReactNode } from "react";
import "./breadcrumb.css";

export type BreadcrumbEntry = {
  label: ReactNode;
  /** Where the crumb leads. The last item may leave it out. */
  href?: string;
};

export type BreadcrumbProps = ComponentProps<"nav"> & {
  /** From the root down to the current page. The last item is marked as the current page. */
  items: BreadcrumbEntry[];
};

export function Breadcrumb({ items, className, "aria-label": label = "Breadcrumb", ...rest }: BreadcrumbProps) {
  return (
    <nav {...rest} aria-label={label} className={["eq-breadcrumb", className].filter(Boolean).join(" ")}>
      <ol className="eq-breadcrumb-list">
        {items.map((item, i) => {
          const current = i === items.length - 1;
          // Middle crumbs may be cut short; keep the full text on hover.
          const title = typeof item.label === "string" ? item.label : undefined;
          return (
            <li key={i} className="eq-breadcrumb-item">
              {item.href !== undefined ? (
                <a
                  className="eq-breadcrumb-link"
                  href={item.href}
                  title={title}
                  aria-current={current ? "page" : undefined}
                >
                  {item.label}
                </a>
              ) : (
                <span className="eq-breadcrumb-text" title={title} aria-current={current ? "page" : undefined}>
                  {item.label}
                </span>
              )}
            </li>
          );
        })}
      </ol>
    </nav>
  );
}
components/equip/breadcrumb.css85 lines
@layer equip {
  .eq-breadcrumb {
    min-width: 0;
    max-width: 100%;
    font: var(--eq-weight) var(--eq-text-s) / 1.4 var(--eq-font);
  }

  .eq-breadcrumb-list {
    display: flex;
    align-items: center;
    min-width: 0;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* Crumbs shrink before they wrap. The first stays whole; the middle ones go first. */
  .eq-breadcrumb-item {
    display: flex;
    align-items: center;
    flex: 0 1 auto;
    min-width: 0;
    color: var(--eq-ink-2);
  }

  .eq-breadcrumb-item:first-child {
    flex-shrink: 0;
  }

  /* A huge shrink factor makes the middle crumbs reach their floor before the last one gives
     anything. The floor covers the separator and its margins plus a few characters. */
  .eq-breadcrumb-item:not(:first-child, :last-child) {
    flex-shrink: 1000;
    min-width: calc(2 * var(--eq-space-2) + 6ch);
  }

  .eq-breadcrumb-item:not(:first-child, :last-child) > :is(.eq-breadcrumb-link, .eq-breadcrumb-text) {
    max-inline-size: 16ch;
  }

  /* The separator is drawn by CSS and given empty alt text, so it is never read out. */
  .eq-breadcrumb-item + .eq-breadcrumb-item::before {
    content: "/";
    content: "/" / "";
    flex: none;
    margin-inline: var(--eq-space-2);
    color: var(--eq-ink-3);
  }

  .eq-breadcrumb-link,
  .eq-breadcrumb-text {
    display: block;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .eq-breadcrumb-link {
    min-height: 24px;
    line-height: 24px;
    border-radius: var(--eq-radius-s);
    color: inherit;
    text-decoration: none;
    transition: color var(--eq-dur) var(--eq-ease);
  }

  @media (hover: hover) {
    .eq-breadcrumb-link:hover {
      color: var(--eq-ink);
      text-decoration: underline;
      text-underline-offset: 3px;
    }
  }

  .eq-breadcrumb-link:focus-visible {
    outline: 2px solid var(--eq-focus);
    outline-offset: 2px;
  }

  .eq-breadcrumb [aria-current="page"] {
    color: var(--eq-ink);
    font-weight: var(--eq-weight-strong);
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index