Breadcrumb

Shows where the current page sits in a hierarchy and leads back up to any level above it (also called a trail or path). Where the trail runs out of room, its middle levels fold into a menu.

import { Breadcrumb } from "@umriss-ui/core";

The last level is the current page: marked as such and not a link. Routing is yours: a level with an href alone is followed by the browser, one with onSelect runs your router instead.

Examples

A trail

Pass the items from the top down; the last is the current page, marked as such and not a link.

import { Breadcrumb } from "@umriss-ui/core";

export default function ATrail() {
  return (
    <Breadcrumb
      items={[
        { label: "Finance", href: "#/breadcrumb" },
        { label: "Cost centres", href: "#/breadcrumb" },
        { label: "Marketing, CC-1200" },
      ]}
    />
  );
}

Route it yourself

Give a level onSelect and your router runs instead of the browser; with an href beside it, a middle click still opens a tab.

import { useState } from "react";
import { Breadcrumb, Button, Stack, Text } from "@umriss-ui/core";

const PATH = ["North depot", "Tour T-01", "Stop 4", "Shipment FP-1004249"];

export default function RouteItYourself() {
  const [depth, setDepth] = useState(PATH.length);
  const trail = PATH.slice(0, depth);

  return (
    <Stack gap={3} align="flex-start">
      <Breadcrumb
        items={trail.map((label, i) => ({ label, onSelect: () => setDepth(i + 1) }))}
        style={{ alignSelf: "stretch" }}
      />
      <Text size="xs" tone="muted">
        Page: {trail[trail.length - 1]}
      </Text>
      <Button size="sm" disabled={depth === PATH.length} onClick={() => setDepth(PATH.length)}>
        Back to the shipment
      </Button>
    </Stack>
  );
}

Fold when narrow

A long trail in a narrow place folds its middle levels into a menu, measured, keeping the first level and the current page.

import { Breadcrumb, Card, CardBody, Stack, Text } from "@umriss-ui/core";

const TRAIL = [
  { label: "Quillmere", href: "#/breadcrumb" },
  { label: "Services", href: "#/breadcrumb" },
  { label: "Payments", href: "#/breadcrumb" },
  { label: "Checkout", href: "#/breadcrumb" },
  { label: "Incidents", href: "#/breadcrumb" },
  { label: "INC-1048", href: "#/breadcrumb" },
  { label: "Timeline" },
];

export default function FoldWhenNarrow() {
  return (
    <Stack gap={4}>
      {[300, 480].map((width) => (
        <Card key={width} style={{ width: "100%", maxWidth: width }}>
          <CardBody>
            <Stack gap={2}>
              <Text size="xs" tone="muted">
                Up to {width} pixels
              </Text>
              <Breadcrumb items={TRAIL} />
            </Stack>
          </CardBody>
        </Card>
      ))}
    </Stack>
  );
}

Browse down and back up

Each level opens the next and the trail is the way back; narrow it with the slider to watch the fold measure itself.

import { useState } from "react";
import { Breadcrumb, Button, Card, CardBody, FormField, Grid, Heading, Slider, Stack, Text } from "@umriss-ui/core";

/* Data from the controlling world, written out here so the example runs on its own. */
const on = (month: number, day: number) => new Date(2026, month - 1, day).getTime();

interface CostCentre {
  id: string;
  name: string;
  owner: string;
  /** The budget of one month. */
  monthlyBudget: number;
}

const COST_CENTRES: readonly CostCentre[] = [
  { id: "CC-1100", name: "Sales", owner: "Helen Marsh", monthlyBudget: 142_000 },
  { id: "CC-1200", name: "Marketing", owner: "Rafael Ortiz", monthlyBudget: 68_000 },
  { id: "CC-2100", name: "Engineering", owner: "Anika Sørensen", monthlyBudget: 188_000 },
  { id: "CC-2200", name: "Design", owner: "Paul Whitaker", monthlyBudget: 54_000 },
  { id: "CC-3100", name: "Customer service", owner: "Grace Obi", monthlyBudget: 61_000 },
  { id: "CC-4100", name: "Finance", owner: "Martina Vogel", monthlyBudget: 47_000 },
  { id: "CC-4200", name: "People", owner: "Daniel Frost", monthlyBudget: 39_000 },
  { id: "CC-4300", name: "IT", owner: "Kenji Arai", monthlyBudget: 83_000 },
  { id: "CC-4400", name: "Facilities", owner: "Olga Ivanova", monthlyBudget: 72_000 },
];

interface InvoiceLine {
  description: string;
  quantity: number;
  unitPrice: number;
  /** A fraction: 0.1 is ten per cent off. */
  discount: number;
  /** A fraction: 0.19 or 0.07, or 0 where no VAT is charged. */
  vatRate: number;
}

interface Invoice {
  id: string;
  supplier: string;
  costCentre: string;
  received: number;
  due: number;
  status: "awaiting approval" | "approved" | "paid" | "rejected";
  lines: readonly InvoiceLine[];
}

const INVOICES: readonly Invoice[] = [
  {
    id: "INV-26-0318", supplier: "Brandlow Office Supply", costCentre: "CC-4400", received: on(3, 16), due: on(4, 15), status: "awaiting approval",
    lines: [
      { description: "Desk lamps, LED", quantity: 24, unitPrice: 48.9, discount: 0.1, vatRate: 0.19 },
      { description: "Printer paper, box of 5 reams", quantity: 40, unitPrice: 21.5, discount: 0, vatRate: 0.19 },
      { description: "Delivery", quantity: 1, unitPrice: 35, discount: 0, vatRate: 0.19 },
    ],
  },
  {
    id: "INV-26-0317", supplier: "Kettering & Shaw Events", costCentre: "CC-1200", received: on(3, 13), due: on(4, 12), status: "awaiting approval",
    lines: [
      { description: "Trade fair stand, 3 days", quantity: 1, unitPrice: 12_400, discount: 0.05, vatRate: 0.19 },
      { description: "Catering, per guest", quantity: 180, unitPrice: 18.5, discount: 0, vatRate: 0.07 },
    ],
  },
  {
    id: "INV-26-0309", supplier: "Nimbrel Software", costCentre: "CC-4300", received: on(3, 9), due: on(4, 8), status: "approved",
    lines: [
      { description: "Design tool licences, annual", quantity: 12, unitPrice: 540, discount: 0.15, vatRate: 0.19 },
      { description: "Onboarding session", quantity: 2, unitPrice: 890, discount: 0, vatRate: 0.19 },
    ],
  },
  {
    id: "INV-26-0302", supplier: "Fenwright Legal", costCentre: "CC-4100", received: on(3, 2), due: on(3, 16), status: "paid",
    lines: [{ description: "Contract review, hours", quantity: 14.5, unitPrice: 260, discount: 0, vatRate: 0.19 }],
  },
  {
    id: "INV-26-0226", supplier: "Corrin Travel", costCentre: "CC-1100", received: on(2, 26), due: on(3, 12), status: "paid",
    lines: [
      { description: "Rail tickets, sales conference", quantity: 22, unitPrice: 139, discount: 0, vatRate: 0.07 },
      { description: "Hotel, nights", quantity: 44, unitPrice: 112, discount: 0.08, vatRate: 0.07 },
    ],
  },
  {
    id: "INV-26-0221", supplier: "Stellbrook Consulting", costCentre: "CC-2100", received: on(2, 21), due: on(3, 23), status: "rejected",
    lines: [{ description: "Architecture review, days", quantity: 6, unitPrice: 1450, discount: 0, vatRate: 0 }],
  },
];

interface Place {
  name: string;
  children?: Place[];
}

const DIVISIONS = [
  { prefix: "CC-1", name: "Commercial" },
  { prefix: "CC-2", name: "Product" },
  { prefix: "CC-3", name: "Service" },
  { prefix: "CC-4", name: "Administration" },
];

/* The company, its divisions, their cost centres, each centre's invoices and
   each invoice's lines. */
const COMPANY: Place = {
  name: "Carrow & Lisle",
  children: DIVISIONS.map((division) => ({
    name: division.name,
    children: COST_CENTRES.filter((centre) => centre.id.startsWith(division.prefix)).map((centre) => ({
      name: `${centre.name}, ${centre.id}`,
      children: INVOICES.filter((invoice) => invoice.costCentre === centre.id).map((invoice) => ({
        name: `${invoice.id} · ${invoice.supplier}`,
        children: invoice.lines.map((line) => ({ name: line.description })),
      })),
    })),
  })),
};

/** The places from the company down to the first one of this name. */
function pathTo(name: string, path: Place[]): Place[] | undefined {
  const here = path[path.length - 1]!;
  if (here.name === name) return path;
  for (const child of here.children ?? []) {
    const found = pathTo(name, [...path, child]);
    if (found) return found;
  }
  return undefined;
}

export default function BrowseDownAndBackUp() {
  const [path, setPath] = useState<Place[]>(() => pathTo("Desk lamps, LED", [COMPANY]) ?? [COMPANY]);
  const [width, setWidth] = useState(640);
  const here = path[path.length - 1]!;

  return (
    <Stack gap={4}>
      <FormField label="Width of the trail" style={{ maxWidth: 360 }}>
        <Slider min={200} max={640} step={20} value={width} onChange={setWidth} format={(v) => `${v} px`} />
      </FormField>
      <Card>
        <CardBody>
          <Stack gap={4}>
            <div style={{ width, maxWidth: "100%" }}>
              <Breadcrumb
                items={path.map((place, i) => ({ label: place.name, onSelect: () => setPath(path.slice(0, i + 1)) }))}
              />
            </div>
            <Heading level={3} size="lg">
              {here.name}
            </Heading>
            {here.children && here.children.length > 0 ? (
              <Grid minItemWidth="180px" gap={2}>
                {here.children.map((child) => (
                  <Button key={child.name} onClick={() => setPath([...path, child])}>
                    {child.name}
                  </Button>
                ))}
              </Grid>
            ) : (
              <Text size="sm" tone="muted">
                Nothing further below {here.name}.
              </Text>
            )}
          </Stack>
        </CardBody>
      </Card>
    </Stack>
  );
}

When to use something else

Keyboard

Key Action
Tab Moves through the levels, and to the button of the folded ones.
Enter Follows a level; on the button, opens the menu of folded levels.
Arrow Down / Arrow Up Walks the menu of folded levels.
Escape Closes the menu and returns the focus to its button.

API

BreadcrumbProps

NameTypeDefaultDescription

Also takes every attribute of the rendered element.

BreadcrumbEntry

NameTypeDefaultDescription

Events

NameTypeDefaultDescription

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index