# Breadcrumb

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/breadcrumb/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

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.

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

Demo page: https://romanhaendler.github.io/umriss-ui/core/breadcrumb/

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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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

- The steps of a procedure, not places in a hierarchy → Stepper
- The whole hierarchy to browse, not the path to one place → TreeView

## 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`

| Prop | Type | Default | Description |
|---|---|---|---|
| `items` *required* | `readonly `[`BreadcrumbEntry`](https://romanhaendler.github.io/umriss-ui/core/breadcrumb/#type-BreadcrumbEntry)`[]` | — | The trail from the root to the current page; the last is the page.<br>Shown in: [A trail](https://romanhaendler.github.io/umriss-ui/core/breadcrumb/#a-trail), [Route it yourself](https://romanhaendler.github.io/umriss-ui/core/breadcrumb/#route-it-yourself), [Fold when narrow](https://romanhaendler.github.io/umriss-ui/core/breadcrumb/#fold-when-narrow) and 3 more |
| `ref` | `Ref<HTMLElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

Also takes every attribute of the rendered element.

### `BreadcrumbEntry`

| Prop | Type | Default | Description |
|---|---|---|---|
| `label` *required* | `ReactNode` | — | The level's name, as the trail and the menu of folded levels show it.<br>Shown in: [A trail](https://romanhaendler.github.io/umriss-ui/core/breadcrumb/#a-trail), [Route it yourself](https://romanhaendler.github.io/umriss-ui/core/breadcrumb/#route-it-yourself), [Browse down and back up](https://romanhaendler.github.io/umriss-ui/core/breadcrumb/#browse-down-and-back-up) and 2 more |
| `href` | `string` | — | The level's address. Alone, the browser follows it.<br>Shown in: [A trail](https://romanhaendler.github.io/umriss-ui/core/breadcrumb/#a-trail), [Resolve an incident (Scenarios)](https://romanhaendler.github.io/umriss-ui/core/#resolve-an-incident), [Set up a team (Scenarios)](https://romanhaendler.github.io/umriss-ui/core/#set-up-a-team) |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| `onSelect` | `() => void` | — | Routing is the caller's: given, it runs instead of the browser's navigation - on the link and in the menu of folded levels. A level with `onSelect` and no `href` is a button. In the menu a level is a menu entry, not a link: an `href` alone is followed there by `location.assign`, without a new tab or a middle click - whoever needs those routes through `onSelect`.<br>Shown in: [Route it yourself](https://romanhaendler.github.io/umriss-ui/core/breadcrumb/#route-it-yourself), [Browse down and back up](https://romanhaendler.github.io/umriss-ui/core/breadcrumb/#browse-down-and-back-up) |

## Known limits

- It neither routes nor reads the address: the trail is what you pass.

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
