Skip to content

Breadcrumb

  • Pages deep in a hierarchy, such as Docs / Components / Breadcrumb
  • Letting people go up a level without the back button

Not on a flat site, or as the only navigation.

Choosing between moving around:

ComponentUse it forFor example
BreadcrumbShowing where the page sits in a hierarchy, with links up itHome / Projects / Website
PaginationMoving between pages of one long listSearch results, page 3 of 12
TabsSwitching between views within one pageOverview / Activity / Settings
LinkOne destination“Read the docs”
import { Breadcrumb } from '@arun-dev/ui';
import { docsUrl } from '@/lib/url';
export default function BreadcrumbBasics() {
return (
<Breadcrumb.Root>
<Breadcrumb.Item>
<Breadcrumb.Link href={docsUrl('/')}>Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
<Breadcrumb.Link href={docsUrl('/components/button/')}>Components</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
<Breadcrumb.Current>Breadcrumb</Breadcrumb.Current>
</Breadcrumb.Item>
</Breadcrumb.Root>
);
}

Breadcrumb is a <nav aria-label="Breadcrumb"> around an ordered list of links. The page you are on comes last, as Breadcrumb.Current: not a link, and marked aria-current="page". The / between steps is drawn by CSS and not read out.

<Breadcrumb.Root>
<Breadcrumb.Item>
<Breadcrumb.Link href="/">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
<Breadcrumb.Current>Settings</Breadcrumb.Current>
</Breadcrumb.Item>
</Breadcrumb.Root>

Pass your router’s link to Breadcrumb.Link through render, as with Link.

The / is the ::before of every step but the first, so a separator of your own is one CSS rule on a class you give Breadcrumb.Root: a character in content, or an icon as a mask painted with currentColor, which keeps --breadcrumb-separator-color in light and dark. Keep the / '' after the content — that empty alternative text is what keeps the separator from being read out.

import type { CSSProperties } from 'react';
import { Breadcrumb, Stack } from '@arun-dev/ui';
import { docsUrl } from '@/lib/url';
// Your own .svg file: `?url` gives its address, as your bundler serves it — a small one inlined as a
// data: URL, which is why the url() below is quoted.
import arrowUrl from './assets/breadcrumb-arrow.svg?url';
/**
* The separator is CSS, the `::before` of every step but the first, so your own is a rule on a
* class you give the Root. Keep the `/ ''` alternative text: the separator stays unread.
*
* A character goes straight into `content`. An icon goes in as a mask, painted with
* `currentColor`, so it keeps `--breadcrumb-separator-color` and follows light and dark — inline
* in the CSS, or from an .svg file, whose address the third trail passes in as a custom property.
*/
const SEPARATOR_CSS = `
.trail-chevron .breadcrumb-item + .breadcrumb-item::before {
content: '›' / '';
}
.trail-icon .breadcrumb-item + .breadcrumb-item::before {
content: '' / '';
inline-size: 1em;
block-size: 1em;
background-color: currentColor;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E")
center / contain no-repeat;
}
.trail-file .breadcrumb-item + .breadcrumb-item::before {
content: '' / '';
inline-size: 1em;
block-size: 1em;
background-color: currentColor;
mask: var(--trail-separator) center / contain no-repeat;
}
`;
function Trail({
className,
label,
style,
}: {
className: string;
label: string;
style?: CSSProperties;
}) {
return (
<Breadcrumb.Root className={className} aria-label={label} style={style}>
<Breadcrumb.Item>
<Breadcrumb.Link href={docsUrl('/')}>Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
<Breadcrumb.Link href={docsUrl('/components/button/')}>Components</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
<Breadcrumb.Current>Breadcrumb</Breadcrumb.Current>
</Breadcrumb.Item>
</Breadcrumb.Root>
);
}
export default function BreadcrumbSeparator() {
return (
<Stack gap="sm">
<style>{SEPARATOR_CSS}</style>
{/* Two trails on one page, so each has its own name. */}
<Trail className="trail-chevron" label="Breadcrumb, chevron" />
<Trail className="trail-icon" label="Breadcrumb, icon" />
<Trail
className="trail-file"
label="Breadcrumb, icon file"
style={{ '--trail-separator': `url("${arrowUrl}")` } as CSSProperties}
/>
</Stack>
);
}
.my-trail .breadcrumb-item + .breadcrumb-item::before {
content: '›' / '';
}

An icon from an .svg file of your own works the same way, as a mask. Point at the file from your stylesheet, or, as the third trail does, import its address and pass it in a custom property:

.my-trail .breadcrumb-item + .breadcrumb-item::before {
content: '' / '';
inline-size: 1em;
block-size: 1em;
background-color: currentColor;
mask: url('/icons/arrow.svg') center / contain no-repeat;
}

As a mask the icon takes the separator’s colour, whatever colour the file draws in. To keep the file’s own colours instead, put it in content: content: url('/icons/arrow.svg') / ''.

Breadcrumb.Root props
PropTypeDefaultDescription
classNamestringConcatenated with the component's own classes, never replacing them.
renderReactElementElement or component to render instead of the default. Props, className, event handlers and ref are merged onto it.
refRef<HTMLElement>Ref to the rendered element. Merged with any ref on the render element.
…restHTMLAttributesAnything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM.
  • With two breadcrumbs on a page, give each its own aria-label.
  • Restyle with the --breadcrumb-* tokens, and the separator with a rule of your own (above).