Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions static/app/components/breadcrumbList/breadcrumbDividerCombo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type {Responsive} from '@sentry/scraps/layout';
import {Container, Flex} from '@sentry/scraps/layout';

import {IconSlashForward} from 'sentry/icons';

interface BreadcrumbDividerComboProps {
children: React.ReactNode;
/** Controls visibility — use responsive values for container-query toggling. */
display?: Responsive<'flex' | 'none'>;
}

/**
* Internal wrapper that pairs a breadcrumb item with a trailing slash divider.
* Not exported — only BreadcrumbList should use this to ensure consistent structure.
*
* The visibility toggle lives on a `Container`, not the inner `Flex`: `Flex`
* defaults every unspecified breakpoint/axis slot to `flex`, which emits an
* always-matching `@media (min-width: 0px)` rule that shadows the container
* query and pins the element visible. `Container` skips unspecified slots, so
* the `@container` query actually drives the collapse.
*/
export function BreadcrumbDividerCombo({children, display}: BreadcrumbDividerComboProps) {
return (
<Container as="li" display={display ?? 'flex'}>
<Flex align="center" gap="xs" flexShrink={0}>
{children}
<Flex as="span" align="center" justify="center" flexShrink={0} aria-hidden>
<IconSlashForward size="md" variant="muted" aria-hidden />
</Flex>
</Flex>
</Container>
);
}
246 changes: 246 additions & 0 deletions static/app/components/breadcrumbList/breadcrumbList.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
---
title: BreadcrumbList
description: A typed, accessible breadcrumb trail that collapses parent links into an overflow menu in narrow containers.
category: navigation
source: 'sentry/components/breadcrumbList'
resources:
js: https://github.com/getsentry/sentry/blob/master/static/app/components/breadcrumbList/breadcrumbList.tsx
figma: https://www.figma.com/design/a638AEl7pFxj29zMODCiOB/Specs--Page-Frame-v1?node-id=1198-17420
---

import {LeadingGraphic} from '@sentry/scraps/leadingGraphic';

import {BreadcrumbList} from 'sentry/components/breadcrumbList';
import {IconSettings, IconStar} from 'sentry/icons';
import * as Storybook from 'sentry/stories';

export const documentation =
import('!!type-loader!sentry/components/breadcrumbList/breadcrumbList');

`BreadcrumbList` renders a horizontal breadcrumb trail as inline content. It is designed to sit inside the page's heading (e.g. the TopBar title `<h1>`), which owns the landmark and heading semantics — so the trail itself renders no `<nav>` landmark and the current-page crumb is inline text, not its own heading. Each slot in the trail has an explicit `type` — developers must consciously choose a variant rather than having it inferred from position. At container widths below 800px, parent link crumbs automatically collapse into a single overflow (`…`) menu via a container query, keeping the page title always visible.

## Usage

```tsx
import {BreadcrumbList} from 'sentry/components/breadcrumbList';
```

Pass a typed `items` array. Each entry is a discriminated union with a `type` key and a `props` object matching that type's interface.

<Storybook.Demo>
<BreadcrumbList
items={[
{type: 'link', props: {label: 'Organization', to: '/organizations/sentry/'}},
{type: 'link', props: {label: 'Projects', to: '/organizations/sentry/projects/'}},
{
type: 'link',
props: {label: 'javascript', to: '/settings/sentry/projects/javascript/'},
},
{type: 'page-title', props: {label: 'General Settings'}},
]}
/>
</Storybook.Demo>
```tsx
<BreadcrumbList
items={[
{type: 'link', props: {label: 'Organization', to: '/organizations/sentry/'}},
{type: 'link', props: {label: 'Projects', to: '/organizations/sentry/projects/'}},
{
type: 'link',
props: {label: 'javascript', to: '/settings/sentry/projects/javascript/'},
},
{type: 'page-title', props: {label: 'General Settings'}},
]}
/>
```

## Item types

### `'link'` — parent page

A clickable, muted-weight crumb that navigates to a parent page. Use for every item except the current page. Set `preservePageFilters` to carry project/environment/date filters across the navigation. Labels are truncated at 132px and wrapped in a tooltip showing the full text.

<Storybook.Demo>
<BreadcrumbList
items={[
{
type: 'link',
props: {
label: 'A very long project name that will be truncated',
to: '/organizations/sentry/projects/',
},
},
{type: 'page-title', props: {label: 'Current Page'}},
]}
/>
</Storybook.Demo>
```tsx
{type: 'link', props: {label: 'Projects', to: '/organizations/sentry/projects/'}}
{type: 'link', props: {label: 'Issues', to: '/issues/', preservePageFilters: true}}
```

### `'page-title'` — current page

The final crumb — bold, non-interactive, and always visible. Represents the page the user is currently on. Labels are truncated at 200px and wrapped in a tooltip showing the full text.

<Storybook.Demo>
<BreadcrumbList
items={[
{type: 'link', props: {label: 'Issues', to: '/organizations/sentry/issues/'}},
{
type: 'page-title',
props: {label: 'TypeError: Cannot read properties of undefined'},
},
]}
/>
</Storybook.Demo>
```tsx
{type: 'page-title', props: {label: 'TypeError: Cannot read properties of undefined'}}
```

### `'select-projects'` — project picker

A `CompactSelect` trigger used in settings pages where the user can switch between projects inline. Accepts standard `options`, `value`, and `onChange` props.

<Storybook.Demo>
<BreadcrumbList
items={[
{type: 'link', props: {label: 'Settings', to: '/settings/'}},
{
type: 'select-projects',
props: {
value: 'javascript',
options: [
{value: 'javascript', label: 'javascript'},
{value: 'python', label: 'python'},
{value: 'ruby', label: 'ruby'},
],
onChange: () => {},
},
},
{type: 'page-title', props: {label: 'Client Keys (DSN)'}},
]}
/>
</Storybook.Demo>
```tsx
{
type: 'select-projects',
props: {
value: 'javascript',
options: [{value: 'javascript', label: 'javascript'}, {value: 'python', label: 'python'}],
onChange: option => navigate(`/settings/${org.slug}/projects/${option.value}/`),
},
}
```

## Leading graphics

```tsx
import {LeadingGraphic} from '@sentry/scraps/leadingGraphic';
```

All item types accept an optional `leadingGraphic` prop typed as `React.ReactElement<LeadingGraphicProps>`. Pass a `<LeadingGraphic />` element — platform icons, Sentry icons, or avatars.

<Storybook.Demo>
<BreadcrumbList
items={[
{
type: 'link',
props: {
label: 'Settings',
to: '/settings/',
leadingGraphic: (
<LeadingGraphic variant="icon" icon={<IconSettings size="md" />} />
),
},
},
{
type: 'link',
props: {
label: 'javascript',
to: '/settings/sentry/projects/javascript/',
leadingGraphic: (
<LeadingGraphic variant="badge-project" projectPlatforms={['javascript']} />
),
},
},
{
type: 'page-title',
props: {
label: 'Client Keys (DSN)',
leadingGraphic: <LeadingGraphic variant="icon" icon={<IconStar size="md" />} />,
},
},
]}
/>
</Storybook.Demo>
```tsx leadingGraphic=
{<LeadingGraphic variant="icon" icon={<IconSettings size="md" />} />}
leadingGraphic=
{<LeadingGraphic variant="badge-project" projectPlatforms={['javascript']} />}
leadingGraphic={<LeadingGraphic variant="icon" icon={<IconStar size="md" />} />}
```

## Pagination

The `'page-title'` item accepts an optional `pagination` prop for pages where the user can step through a list of items (e.g. issues, replays, traces). Pass a `{previous, next}` object — each with a `to` URL, `ariaLabel`, optional `disabled` flag, and an optional rich `tooltip`. The two chevron buttons are rendered internally.

<Storybook.Demo>
<BreadcrumbList
items={[
{type: 'link', props: {label: 'Issues', to: '/organizations/sentry/issues/'}},
{
type: 'page-title',
props: {
label: 'TypeError: Cannot read properties of undefined',
pagination: {
previous: {ariaLabel: 'Previous issue', to: '/issues/122/'},
next: {ariaLabel: 'Next issue', to: '/issues/124/', disabled: true},
},
},
},
]}
/>
</Storybook.Demo>
```tsx pagination=
{{
previous: {ariaLabel: 'Previous issue', to: '/issues/122/'},
next: {ariaLabel: 'Next issue', to: '/issues/124/', disabled: true},
}}
```

## Trailing actions

The `'page-title'` item also accepts a `trailingActions` slot for up to 52px of action buttons rendered after the label — e.g. a bookmark or share button.

```tsx
trailingActions={
<Button size="zero" priority="transparent" icon={<IconStar size="md" />} aria-label="Bookmark" />
}
```

## Overflow (container query)

`BreadcrumbList` sets `containerType="inline-size"` on its outer element. When that container is narrower than 800px, every parent item is hidden and only the `'page-title'` stays visible. `'link'` parents are additionally replaced with a single `…` overflow button; other parent types (e.g. `'select-projects'`) simply collapse out of view — they are not added to the overflow menu.

Drag the sizing handle below to see the transition:

<Storybook.SizingWindow display="block">
<BreadcrumbList
items={[
{type: 'link', props: {label: 'Settings', to: '/settings/'}},
{
type: 'link',
props: {label: 'javascript', to: '/settings/sentry/projects/javascript/'},
},
{
type: 'link',
props: {
label: 'Client Keys (DSN)',
to: '/settings/sentry/projects/javascript/keys/',
},
},
{type: 'page-title', props: {label: 'web-frontend-dsn'}},
]}
/>
</Storybook.SizingWindow>
Loading
Loading