Navigation

Breadcrumb

Hierarchical navigation trail with separators

Preview

size
items

Code

import css from "./Breadcrumb.module.css";

import { CaretRightIcon } from "@phosphor-icons/react/dist/csr/CaretRight";

type Size = "sm" | "md";

interface ResponsiveSize {
  base: Size;
  mobile?: Size;
  tablet?: Size;
  desktop?: Size;
  wide?: Size;
}

interface BreadcrumbItem {
  label: string;
  href?: string;
}

interface BreadcrumbProps {
  items: BreadcrumbItem[];
  size: Size | ResponsiveSize;
  "aria-label": string;
}

function getSizeClasses(size: Size | ResponsiveSize): string {
  if (typeof size === "string") {
    return css[size];
  }

  const classes = [css[`${size.base}_base`]];

  if (size.mobile) classes.push(css[`${size.mobile}_mobile`]);
  if (size.tablet) classes.push(css[`${size.tablet}_tablet`]);
  if (size.desktop) classes.push(css[`${size.desktop}_desktop`]);
  if (size.wide) classes.push(css[`${size.wide}_wide`]);

  return classes.join(" ");
}

export default function Breadcrumb({
  items,
  size,
  "aria-label": ariaLabel,
}: BreadcrumbProps) {
  return (
    <nav
      className={`
        ${css.breadcrumb}
        ${getSizeClasses(size)}
      `}
      aria-label={ariaLabel}
    >
      <ol className={css.breadcrumb_list} role="list">
        {items.map((item, index) => {
          const last = index === items.length - 1;
          const Tag = item.href ? "a" : "span";

          return (
            <li key={item.label} className={css.breadcrumb_item}>
              {last ? (
                <span className={css.breadcrumb_current} aria-current="page">
                  {item.label}
                </span>
              ) : (
                <Tag className={css.breadcrumb_link} href={item.href}>
                  {item.label}
                </Tag>
              )}

              {!last && (
                <CaretRightIcon
                  weight="bold"
                  aria-hidden="true"
                  className={css.breadcrumb_separator}
                />
              )}
            </li>
          );
        })}
      </ol>
    </nav>
  );
}

Props

PropTypeRequiredDescription
itemsBreadcrumbItem[]requiredOrdered trail: { label, href? } per level. The last one is the current page (aria-current); a level without href renders as plain text.
size"sm" | "md" | ResponsiveSizerequiredText and separator size. Fixed or responsive.
aria-labelstringrequiredName of the <nav> for screen readers; it tells this navigation apart from the others on the page.