Brik Design System
Components

Breadcrumb

Navigation trail showing current page location within a hierarchy.

Breadcrumb shows where the user is in a hierarchical structure — Home → Products → Design System → Button. Each crumb (except the last) is a link back up the tree; the last is plain text marked as the current page.

For sibling navigation between sections, use TabBar. For flat top-level nav, use TopNavigation or SidebarNavigation.

Use it for

  • Page headers in apps with hierarchical navigation
  • Settings page section position
  • Documentation site current-location markers
  • Any place users benefit from "where am I" + "how do I go up"

Import

import { Breadcrumb } from '@brikdesigns/bds';

Variants

Slash separator

<Breadcrumb
  items={[
    { label: 'Home', href: '/' },
    { label: 'Products', href: '/products' },
    { label: 'Design System' },
  ]}
/>

Chevron separator

<Breadcrumb
  items={[
    { label: 'Home', href: '/' },
    { label: 'Settings', href: '/settings' },
    { label: 'Profile' },
  ]}
  separator="chevron"
/>

Deep nesting

For 5+ level hierarchies, consider truncating with an ellipsis or root-only fallback — Breadcrumb itself doesn't auto-truncate; the consumer trims items before passing.

<Breadcrumb
  items={[
    { label: 'Home', href: '/' },
    { label: '…', href: '/clients' },
    { label: 'Acme Co', href: '/clients/acme' },
    { label: 'Engagements', href: '/clients/acme/engagements' },
    { label: 'Brand Refresh' },
  ]}
/>

Switcher

Pass two or more options (plus a switchLabel) to render a caret after the trail. It opens a menu of sibling records — another service in the same line, another project on the same client — so users jump between them without navigating back to an index page. Fewer than two options renders no caret and the trail stays a plain breadcrumb. The trailing crumb keeps aria-current="page"; the caret is a separate aria-haspopup="menu" control alongside it.

onNavigate routes the selection client-side; without it BDS falls back to a full-page navigation (window.location.href).

<Breadcrumb
  items={[
    { label: 'Home', href: '/' },
    { label: 'Services', href: '/services' },
    { label: 'Brand strategy' },
  ]}
  options={[
    { label: 'Brand strategy', href: '/services/brand-strategy', current: true },
    { label: 'Brand identity', href: '/services/brand-identity' },
    { label: 'Brand guidelines', href: '/services/brand-guidelines' },
  ]}
  switchLabel="Switch service"
  onNavigate={(href) => router.push(href)}
/>

Per-crumb switcher

The switcher above is scoped to the trailing crumb. Any crumb — including a MIDDLE one — can carry its own, independent switcher by passing switcher on that item instead: e.g. the "client" crumb on an admin project page switching between clients, while a separate trailing switcher (or none) handles project-to-project. It renders inline right after that crumb's label/link, before the separator to the next crumb, using the same caret + menu, the same two-or-more-options floor, and the same BreadcrumbSwitchOption shape as the trailing switcher. A trail can have neither, either, or both — each switcher keeps its own open/close state, so one never affects the other.

switcher.switchLabel is required (not optional, unlike the top-level switchLabel) — a trail can carry more than one per-crumb switcher, so each needs its own distinguishing accessible name rather than a shared generic fallback.

<Breadcrumb
  items={[
    { label: 'Home', href: '/' },
    { label: 'Clients', href: '/clients' },
    {
      label: 'Acme Holdings',
      href: '/clients/acme-holdings',
      switcher: {
        options: [
          { label: 'Acme Holdings', href: '/clients/acme-holdings', current: true },
          { label: 'Beacon Group', href: '/clients/beacon-group' },
          { label: 'Summit Partners', href: '/clients/summit-partners' },
        ],
        switchLabel: 'Switch client',
        onNavigate: (href) => router.push(href),
      },
    },
    { label: 'Projects', href: '/clients/acme-holdings/projects' },
    { label: 'Brand Refresh' },
  ]}
/>

Search, filters and scope label

A long switcher menu — every client in a practice, say — can be narrowed in three ways. They work on the trailing options and on a per-crumb switcher alike.

  • Search. A labelled search box appears once the option count is greater than searchThreshold (default 8; 0 always shows it). It matches the option label, case-insensitively, and clears when the menu closes.
  • Filters. Pass filters to render FilterToggles, off by default. Turning one on keeps only options whose facets[facet] === value; the current option always stays visible. Filtering is client-side — BDS fetches nothing.
  • Scope label. scopeLabel names the list, rendered above it.

Options can carry a hint — secondary text under the label — which is display only and not searched. Search takes focus when the menu opens; Escape closes the menu and returns focus to the caret.

<Breadcrumb
  items={[
    { label: 'Home', href: '/' },
    { label: 'Acme Dental', href: '/clients/acme-dental' },
    { label: 'Listings' },
  ]}
  options={clients.map((c) => ({
    label: c.name,
    href: `/clients/${c.slug}/listings`,
    current: c.slug === 'acme-dental',
    hint: c.industry,
    facets: { industry: c.industrySlug },
  }))}
  switchLabel="Switch client"
  scopeLabel="Clients with Online Business Listings"
  filters={[
    { id: 'same-industry', label: 'Same industry', facet: 'industry', value: 'dental' },
  ]}
/>

Service-line cascade

When a breadcrumb sits inside a [data-service-line='X'] subtree, the current-page label and separators rebind canonical --text-secondary and --text-muted to the matching --text-service-{name} value (brand, marketing, information, product, and service → back-office).

The component itself stays scope-blind — only the cascade block in Breadcrumb.css knows about service lines. Consumers set data-service-line on any ancestor (page wrapper, hero card, sidebar column) and the breadcrumb inside picks up the hue. The legacy data-audience attribute still resolves during the deprecation window (#788).

<div data-service-line="marketing">
  <Breadcrumb items={[
    { label: 'Services', href: '/services' },
    { label: 'Website Design' },
  ]} />
</div>

When not to use

  • Don't use Breadcrumb when there's no hierarchy. Single-level apps don't benefit — drop the trail entirely.
  • Don't use Breadcrumb as primary navigation. It's a contextual aid, not a nav bar. Use TopNavigation / SidebarNavigation for primary nav.
  • Don't make the last item a link. It represents the current page; clicking it would reload to itself.

Accessibility

  • Renders a <nav aria-label="Breadcrumb"> element.
  • The last item carries aria-current="page" and renders as plain text — not a link.
  • Earlier items render as <a> when href is set.
  • Separators are decorative (aria-hidden).
  • Switcher search is labelled ("Search options"), each filter toggle is a button with aria-pressed and its label as its name, and an empty result is announced through a status region.

API

PropTypeDefault
itemsBreadcrumbItem[] (required)—
separator'slash' | 'chevron''slash'
linkComponentBdsLinkComponent—
optionsBreadcrumbSwitchOption[]— (two or more render the switcher caret)
switchLabelstring— (required when options is passed)
onNavigate(href: string, option: BreadcrumbSwitchOption) => voidundefined (window.location.href = href)
filtersBreadcrumbSwitchFilter[]— (each renders a FilterToggle, off by default)
scopeLabelstring— (rendered above the list)
searchThresholdnumber8 (search shows when the option count is greater; 0 always shows it)

Plus standard <nav> HTML attributes.

interface BreadcrumbItem {
  label: ReactNode;
  href?: string;
  switcher?: {
    options: BreadcrumbSwitchOption[];
    switchLabel: string;
    onNavigate?: (href: string, option: BreadcrumbSwitchOption) => void;
    filters?: BreadcrumbSwitchFilter[];
    scopeLabel?: string;
    searchThreshold?: number;
  };
}
interface BreadcrumbSwitchOption {
  label: string;
  href: string;
  /** The record currently being viewed — highlighted and non-navigating. */
  current?: boolean;
  /** Secondary text under the label. Display only — not searched. */
  hint?: string;
  /** Data a filter matches against, keyed by facet name. Never rendered. */
  facets?: Record<string, string>;
}
interface BreadcrumbSwitchFilter {
  id: string;
  /** Toggle label and accessible name. */
  label: string;
  /** Key into each option's `facets`. */
  facet: string;
  /** Facet value an option must equal while the filter is on. */
  value: string;
}

On this page

💬