Breadcrumbs
Every breadcrumb trail is built from waldur-ui: BreadcrumbTrail for a page's trail, BreadcrumbsProvider and useBreadcrumbs to set it from the page, and the Breadcrumb* primitives for anything custom. Nothing in it knows about a router, so the root app and every micro-app under apps/* use the same components. The Bootstrap/Metronic .breadcrumb stylesheet and react-bootstrap's Breadcrumb are gone, and the waldur-custom/no-bootstrap-breadcrumb lint rule keeps them out.
Stories: Navigation/BreadcrumbTrail, Navigation/Breadcrumb and Data Display/MiddleTruncate in Storybook. Their play functions run in Chromium and cover the layout and keyboard behaviour described below.
Choosing a component
| You need… | Use |
|---|---|
| a page's trail in the root app header | useBreadcrumbs(items) from the page (the header draws it) |
| a trail in a micro-app or anywhere you hold the items yourself | <BreadcrumbTrail items={…} /> |
| a trail fed by pages, with routing, outside the root app | BreadcrumbsProvider + useBreadcrumbs + <BreadcrumbTrail /> |
| a one-off trail with its own markup (no collapse, no menus) | the primitives: Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis |
| the middle of a long name truncated anywhere else | MiddleTruncate |
Setting a page's crumbs
A page passes its crumbs to useBreadcrumbs; they show while the page is mounted and clear when it unmounts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
- Memoise the items. A new array on every render re-sets the trail, and the trail starts expanded and re-measures whenever its items change.
- Reuse the presets.
usePresetBreadcrumbItems()(src/navigation/header/breadcrumb/utils.ts) builds the organization, organizations, organization projects and project crumbs, including the permission check that turns an organization the user can't open into plain text, and the resource-filter sync on click. Pass a second argument to override fields. - Without a provider (a unit test, a micro-app without a trail),
useBreadcrumbsdoes nothing instead of throwing.
IBreadcrumbItem
| Field | Notes |
|---|---|
key |
Unique within the trail. |
text |
The label. A string: the trail measures and truncates it. |
to, params |
Where the crumb links, in the router's terms: see Routing. Omit for a crumb that isn't a link. |
onClick |
With to: runs alongside the navigation. Without: the crumb is a button. |
active |
The current page: not a link, marked aria-current="page", and middle-truncated to fit. Usually the last crumb. |
dropdown |
Content of a popover the crumb opens: a node, or (close) => node to close it from inside (after picking an item). |
hideDropdownArrow |
Drops the caret next to a dropdown crumb. |
truncate |
Caps the label at 320px with an end ellipsis; tooltipText (or the text) becomes its title. |
maxLength |
Shortens a label more than five characters over it to this many (Norther...ortium), with the whole text in a tooltip. |
A crumb with neither to, onClick nor active is plain text, for a level the user can't open.
Routing
The trail turns a crumb's to and params into a link through the renderLink of the nearest BreadcrumbsProvider, or a renderLink prop on the trail itself. It fills the returned element with the crumb's content and classes.
1 2 3 4 5 6 7 8 | |
Without a renderLink, to is used as a plain href.
- Return the link empty and without an
onClick. The trail supplies the content, and attaches the crumb'sonClickin the capture phase: the app'sLinkturns intorole="button"when it is given anonClick. - The root app's
Linkalso hides links to states this deployment has switched off, so a crumb pointing at one renders as text.
Dropdowns
Any crumb can open a popover: a project switcher on the current crumb, an organization switcher on a linked one. The trail owns the popover and its keyboard and focus handling; the content is the app's.
In the root app the content is usually a BreadcrumbDropdown (src/navigation/header/breadcrumb/BreadcrumbDropdown.tsx): a searchable list of links built from an API list call, with filters and favourites. src/project/ProjectBreadcrumbPopover.tsx is the reference. It is not in waldur-ui because it depends on the app's API client, forms and favourites; a micro-app passes its own content.
The popover is non-modal (the page stays clickable while it is open), portaled to <body>, and stacks at z-header-popover.
Layout
The trail is one line that always fits its container:
- Leading crumbs keep their natural width. A
truncatecrumb is capped at 320px. - The last crumb shrinks. When it is the
activeone, its text middle-truncates (Quantum Error Corr…ulations (Create)) withMiddleTruncate, so the end of a long resource name stays readable. - When the leading crumbs don't fit either, everything between the first and the last folds into a
...crumb whose popover lists them. The trail expands again once there is room for the whole of it, and starts expanded whenever its items change. - A trail of one crumb reads "Go back to …" with a back caret.
This needs a width-constrained parent: the trail's container must be a flex item that can shrink (min-w-0), which the root app's header provides. The collapse is measured with a ResizeObserver on the list, so it follows the container, not the window.
Accessibility and keyboard
- The trail is
<nav aria-label="breadcrumb">with an<ol>; separators arearia-hidden. - The current page carries
aria-current="page"once: on its text, or on the crumb itself when the crumb is a switcher. MiddleTruncatehides the head/tail fragments from assistive technology and announces the whole name once.- Dropdown crumbs: the current crumb's label is the trigger (
role="button", in the tab order), so its focus ring doesn't include the separator. Enter and Space open the popover on the keys the crumb's own control ignores: both on the current crumb, Space on a link (Enter follows it), neither on a button (it handles both itself). - Leaving the popover: Escape closes it and returns focus to the crumb. Tab past its last item, or Shift+Tab before its first, closes it and returns focus too, instead of jumping to the end of the page where the panel is portaled. A click elsewhere closes it and leaves focus where the user clicked.
Mobile
Below the md breakpoint the root app draws the trail in a bar of its own under the header (BreadcrumbMain mobile): 55px tall like the header, with its surface, bottom rule and 15px side padding. It sticks on its own right under the header (and the impersonation bar while that shows), and the announcements, warning bars and page content that follow start below it in the page flow. Its measured height is --layout-breadcrumb-bar-height, beside the header's --layout-header-height (navigation/chromeHeights.ts): the page tabs and sticky page content (--layout-sticky-top) sit below both, while the floating drawers hang from the header alone and cover the bar, which stays below the drawer layer (z-99).
Micro-apps
Use BreadcrumbTrail rather than copying anything from src/navigation/header/breadcrumb/. apps/micro-app-poc/src/OrgDashboardMock.tsx passes its crumbs straight to <BreadcrumbTrail items={…} />, with no provider and no router. Add a BreadcrumbsProvider once pages set their own crumbs, with a renderLink if the app has a router. See micro-apps.md.
Testing
- Unit tests render the trail in jsdom (
packages/ui/src/Breadcrumb/BreadcrumbTrail.test.tsx). jsdom lays nothing out: to test the collapse, stubElement.prototype.scrollWidthandResizeObserveras that file does, and usetoas a plain href instead of mocking a router. - Layout and real keyboard behaviour belong in the stories'
playfunctions, which run in Chromium: give the story a fixed width so the overflow is deterministic (BreadcrumbTrail.stories.tsx'sFrame). - In the root app, a page's crumbs are plain data: test the items its hook builds (as
src/navigation/header/breadcrumb/utils.test.tsxdoes) rather than rendering the header.
Migrating off Bootstrap
| Before | After |
|---|---|
react-bootstrap Breadcrumb / Breadcrumb.Item |
BreadcrumbTrail, or the Breadcrumb* primitives |
.breadcrumb, .breadcrumb-item, .breadcrumb-text, … |
nothing: those styles are deleted (the lint rule flags them) |
LayoutContext's breadcrumbs / setBreadcrumbs |
useBreadcrumbItems() / useBreadcrumbs(items) from waldur-ui |
IBreadcrumbItem from @/navigation/types |
IBreadcrumbItem from waldur-ui |
MiddleTruncate from @/core/MiddleTruncate |
MiddleTruncate from waldur-ui |
ellipsis: 'md' \| 'xl' \| 'xxl' on an item |
nothing: the trail collapses by measured overflow |