Breadcrumbs

Tells the user where they are in a nested structure and gives them one click back up.

Navigation Breadcrumbs.jsx Breadcrumbs.d.ts Breadcrumbs.md

Playground

Change a prop and watch it. The code below follows along.

Install and import

Breadcrumbs is in @dovetail-ds/react. Install it with React, if you haven't:

npm install @dovetail-ds/react react react-dom
import { Breadcrumbs } from "@dovetail-ds/react";

Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.

Guidelines

Tells the user where they are in a nested structure and gives them one click back up. Use it for hierarchies at least three levels deep.

Rules

  • The last crumb is the current page. It renders as text with aria-current="page", never as a link to itself.
  • Breadcrumbs reflect hierarchy, not history. Do not build them from the back stack; that is what the browser button is for.
  • Keep labels short. Truncate long titles rather than wrapping the trail onto two lines.
  • Two levels does not need a trail. Use a single back link instead.

Tradeoffs

Deep trails eat a row of vertical space on every page to serve a minority of navigation events. On mobile, consider collapsing the middle crumbs to an ellipsis.

Props

The typed contract, from Breadcrumbs.d.ts.

import * as React from "react";

export interface Crumb {
  label: React.ReactNode;
  href?: string;
  onClick?: (e: React.MouseEvent) => void;
}

/** Shows where the current page sits in the hierarchy. */
export interface BreadcrumbsProps extends React.HTMLAttributes<HTMLElement> {
  /** Root first, current page last. The last item renders as text, not a link. */
  items: Crumb[];
  /** @default "Breadcrumb" */
  label?: string;
  /** @default "/" */
  separator?: React.ReactNode;
}

export declare function Breadcrumbs(props: BreadcrumbsProps): React.JSX.Element;

Tokens it reads

Every custom property Breadcrumbs.jsx resolves, read from the source: 6 semantic, 1 primitive. Component tier first, because that is the one to reach for.

  • semantic Shared roles. Override one and everything using that role moves with it.
  • primitive Raw values. A component reading one directly is a deliberate exception.
TokenTierDeclared as
--dt-space-inline-xssemanticvar(--dt-dim-2)
--dt-text-body-sm-familysemanticvar(--dt-font-family-sans)
--dt-text-body-sm-sizesemanticvar(--dt-font-size-sm)
--dt-text-primarysemanticvar(--dt-color-neutral-900)
--dt-text-secondarysemanticvar(--dt-color-neutral-600)
--dt-text-tertiarysemanticvar(--dt-color-neutral-500)
--dt-font-weight-mediumprimitive500

Source

React only, no dependencies, every value a token.

import React from "react";

export function Breadcrumbs({ items = [], label = "Breadcrumb", separator = "/", style, ...rest }) {
  return (
    <nav aria-label={label} style={style} {...rest}>
      <ol style={{ listStyle: "none", margin: 0, padding: 0, display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--dt-space-inline-xs)" }}>
        {items.map((it, i) => {
          const last = i === items.length - 1;
          return (
            <li key={it.href || i} style={{ display: "flex", alignItems: "center", gap: "var(--dt-space-inline-xs)", minWidth: 0 }}>
              {last ? (
                <span aria-current="page" style={{
                  fontFamily: "var(--dt-text-body-sm-family)", fontSize: "var(--dt-text-body-sm-size)",
                  color: "var(--dt-text-primary)", fontWeight: "var(--dt-font-weight-medium)",
                  overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
                }}>{it.label}</span>
              ) : (
                <a href={it.href} onClick={it.onClick} style={{
                  fontFamily: "var(--dt-text-body-sm-family)", fontSize: "var(--dt-text-body-sm-size)",
                  color: "var(--dt-text-secondary)", textDecoration: "none",
                  overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
                }}>{it.label}</a>
              )}
              {!last && <span aria-hidden="true" style={{ color: "var(--dt-text-tertiary)", fontSize: "var(--dt-text-body-sm-size)" }}>{separator}</span>}
            </li>
          );
        })}
      </ol>
    </nav>
  );
}

In context

Breadcrumbs also appears on the navigation reference cards, alongside the rest of its family.