# Breadcrumb

The path to where you are, whose middle collapses into one affordance rather than off the edge of the strip.

```slint
import { Breadcrumb } from "@glint/components/breadcrumb.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 620px;
    height: 180px;
    background: Tokens.color-background;

    property <string> went: "Nowhere yet.";

    VerticalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: center;

        Breadcrumb {
            accessible-label: "Breadcrumb";
            items: [
                { label: "Home" },
                { label: "Documents" },
                { label: "Invoices" },
                { label: "INV-0042" },
            ];
            navigate(index) => { root.went = "Followed crumb " + index + "."; }
        }

        Text {
            text: root.went;
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
        }
    }
}
```

## Usage

```slint
import { Breadcrumb } from "@glint/components/breadcrumb.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component AppWindow inherits Window {
    width: 520px;
    height: 120px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        alignment: center;

        Breadcrumb {
            accessible-label: "Breadcrumb";
            items: [
                { label: "Home" },
                { label: "Settings" },
                { label: "Billing" },
            ];
            navigate(index) => { debug("go to", index); }
        }
    }
}
```

`items` is the trail, left to right. **The last item is where you are**: it renders as plain foreground text, takes no tab stop and fires nothing. Every crumb before it is a link that fires `navigate` with its index.

A `BreadcrumbItem` carries two fields:

| Field   | What it is                                                                                                                                                                                                 |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | The crumb’s text, and the name assistive technology announces                                                                                                                                              |
| `menu`  | Sibling pages hanging off this crumb — a list of [`MenuSubEntry`](/docs/components/menu-panel#api-reference), the row every Glint menu is built from. Non-empty turns the crumb from a link into a trigger |

Following the crumb is yours. The component reports where the reader wanted to go and changes nothing about the trail: `items` is what you hand it after the route changes.

## Examples

### The separator

`separator` is the glyph between two crumbs, and it is a lucide icon rather than a string — the default is `IconSet.ChevronRight`. An empty `Icon` draws none and takes no column, which is how a trail turns the glyph off instead of swapping it.

```slint
import { Breadcrumb, BreadcrumbItem } from "@glint/components/breadcrumb.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 620px;
    height: 240px;
    background: Tokens.color-background;

    property <[BreadcrumbItem]> trail: [
        { label: "Home" }, { label: "Projects" }, { label: "Glint" },
    ];

    VerticalLayout {
        padding: 24px;
        spacing: 20px;
        alignment: center;

        Breadcrumb {
            accessible-label: "Chevron";
            items: root.trail;
        }

        Breadcrumb {
            accessible-label: "Slash";
            items: root.trail;
            separator: IconSet.Slash;
        }

        Breadcrumb {
            accessible-label: "No separator";
            items: root.trail;
            separator: { paths: [] };
        }
    }
}
```

### A trail too long to draw

`max-visible` caps how many crumbs render whole. Past it the middle of the trail leaves the strip and one ellipsis affordance stands where it was, keeping the first crumb and the last `max-visible - 1`. Two is the floor: a shorter cap would elide the current page along with the middle.

The ellipsis is a control, not a decoration — the run is reachable, not merely gone. Activating it opens the crumbs that left the strip as a picker, and taking one fires `navigate` with its place in the whole trail, not its place in the picker.

```slint
import { Breadcrumb } from "@glint/components/breadcrumb.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 620px;
    height: 220px;
    background: Tokens.color-background;

    property <string> went: "Open the ellipsis to reach the middle of the trail.";

    VerticalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: center;

        Breadcrumb {
            accessible-label: "Deep trail";
            max-visible: 3;
            items: [
                { label: "Home" },
                { label: "Workspace" },
                { label: "Engineering" },
                { label: "Platform" },
                { label: "Releases" },
                { label: "v1.4.0" },
            ];
            navigate(index) => {
                root.went = "Followed crumb " + index + " of the whole trail.";
            }
        }

        Text {
            text: root.went;
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
        }
    }
}
```

### A crumb that opens its siblings

Give a crumb a `menu` and it stops being a link: it draws a chevron, announces itself expandable, and activating it opens a picker of the sibling pages under it. Taking one fires `menu-selected(crumb, row)` — never `navigate`, the way a menu row carrying children never fires itself (ADR-0020). The current page never becomes a trigger.

The rows are plain commands rather than checkable ones. A write into a nested array has nowhere to land in Slint (ADR-0020), which is the same reason `MenuSubEntry` carries no checkbox state — a picker of destinations is all this is.

```slint
import { Breadcrumb } from "@glint/components/breadcrumb.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 620px;
    height: 220px;
    background: Tokens.color-background;

    property <string> picked: "Open “Components” to see its siblings.";

    VerticalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: center;

        Breadcrumb {
            accessible-label: "Docs";
            items: [
                { label: "Docs" },
                { label: "Components", menu: [
                    { label: "Guides" },
                    { label: "Theming" },
                    { label: "Live preview" },
                ] },
                { label: "Breadcrumb" },
            ];
            menu-selected(crumb, row) => {
                root.picked = "Row " + row + " of crumb " + crumb + ".";
            }
        }

        Text {
            text: root.picked;
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
        }
    }
}
```

### Both at once

One picker serves both jobs, so the keyboard, the surface and the highlight are decided once. A trail may elide its middle and still carry menus on the crumbs that stayed.

```slint
import { Breadcrumb } from "@glint/components/breadcrumb.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 640px;
    height: 200px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        alignment: center;

        Breadcrumb {
            accessible-label: "Repository";
            max-visible: 3;
            separator: IconSet.ChevronRight;
            ellipsis-label: "Show the folders in between";
            items: [
                { label: "thelipe7" },
                { label: "glint-ui" },
                { label: "slint" },
                { label: "components", menu: [
                    { label: "internal" },
                    { label: "theme" },
                ] },
                { label: "breadcrumb.slint" },
            ];
        }
    }
}
```

## API Reference

### Properties

| Property            | Type                  | Default                     | Description                                                                                          |
| ------------------- | --------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------- |
| `items`             | `in [BreadcrumbItem]` | no default                  | Crumbs left-to-right; the last item renders as the current page.                                     |
| `separator`         | `in LucideIcon`       | `IconSet.ChevronRight`      | Glyph drawn between crumbs. An empty Icon draws none.                                                |
| `max-visible`       | `in int`              | `0`                         | Longest trail rendered whole; 0 renders every crumb. Past it the middle collapses into the ellipsis. |
| `ellipsis-label`    | `in string`           | `@tr("Show hidden crumbs")` | Name of the ellipsis affordance; override to translate.                                              |
| `highlighted-index` | `in-out int`          | `0`                         | Row the keyboard stands on inside an open picker; consumers rarely set this.                         |

### Callbacks

| Callback                   | Description                                                                                                                                |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `navigate(int)`            | Fired when a crumb is clicked; argument is its index. A picked row of the ellipsis picker fires it too, with the crumb's place in `items`. |
| `menu-selected(int , int)` | Fired with the crumb a picker hung off and the row taken from it.                                                                          |

`BreadcrumbItem` is a data type rather than a component; its two fields are in [Usage](#usage) above. `MenuSubEntry`, the row a crumb’s picker draws, is documented on [MenuPanel’s page](/docs/components/menu-panel#api-reference).

## Accessibility

- **A navigation landmark.** The trail carries `accessible-role: navigation`; naming it is the call site’s, so a page with a breadcrumb and a header nav tells them apart.
- **Earlier crumbs are named controls.** Each is announced by its `label`, answers the accessible default action, and is a tab stop a keyboard user reaches with Tab. Enter and Space follow the focused crumb.
- **A crumb with a menu says so.** It reports `accessible-expandable`, and `accessible-expanded` while its picker is open. Enter or Space opens the picker rather than following the crumb.
- **The current page is text.** The last crumb declares no role of its own, so it is announced as text and never as a control. It keeps its sizing box and still absorbs the clicks that land on it, so nothing behind the trail receives them.
- **The ellipsis is a control.** It is named by `ellipsis-label` (`@tr("Show hidden crumbs")` by default), reports itself expandable, and opens the elided run as the same picker.
- **The picker is a list.** It carries its row count and every row is a named list item. Inside it, ↑ and ↓ move the highlight and wrap at both ends, Home and End jump to its ends, Enter or Space takes the highlighted row, and Esc closes it — the walk every Glint list surface shares.
