Component
Breadcrumbs Max Width
Breadcrumb trail that bounds long labels with max-width truncation while keeping the full text available through the title attribute — no clipped, meaningless navigation.
- Component
- React
- TSX
- Tailwind CSS
- MIT
Preview
Live component
9:41 100%
devsnips.dev/library/react/components/breadcrumbs/breadcrumbs-max-width/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add React/Components/Breadcrumbs/breadcrumbs-max-width React/Components/Breadcrumbs/breadcrumbs-max-width import { createContext, useContext } from "react";
import type {
AnchorHTMLAttributes,
HTMLAttributes,
LiHTMLAttributes,
ReactNode,
} from "react";
/**
* DevSnips React Breadcrumbs — Max Width.
*
* Long labels are truncated inside a bounded max-width so a single verbose
* level never breaks the trail. Truncation uses `truncate` on the label
* span — the full label stays available through the `title` attribute
* (auto-filled from string children, or pass `title` explicitly for
* ReactNode labels), so the meaning is never lost to clipping. On narrow
* screens the list wraps instead of overflowing the page.
*/
function cx(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(" ");
}
const LIST_CLASSES =
"m-0 flex max-w-full list-none flex-wrap items-center gap-x-1 gap-y-0.5 p-0 text-sm leading-5";
const ITEM_CLASSES = "inline-flex min-w-0 items-center gap-1.5";
const LINK_CLASSES =
"inline-flex min-w-0 max-w-[9rem] items-center gap-1.5 rounded-[var(--ds-radius-xs)] text-[var(--ds-color-muted-foreground)] underline-offset-4 sm:max-w-[14rem] transition-colors duration-150 ease-out hover:text-[var(--ds-color-foreground)] hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const CURRENT_CLASSES =
"inline-flex min-w-0 max-w-[9rem] items-center gap-1.5 font-medium text-[var(--ds-color-foreground)] sm:max-w-[14rem]";
const SEPARATOR_CLASSES =
"inline-flex shrink-0 select-none items-center justify-center text-[var(--ds-color-muted-foreground)] [&_svg]:size-3.5";
const ICON_CLASSES = "inline-flex shrink-0 text-[14px] [&_svg]:size-3.5";
const DEFAULT_SEPARATOR = (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="m9 6 6 6-6 6" />
</svg>
);
interface BreadcrumbsContextValue {
separator: ReactNode;
}
const BreadcrumbsContext = createContext<BreadcrumbsContextValue | null>(null);
function useBreadcrumbs(component: string): BreadcrumbsContextValue {
const context = useContext(BreadcrumbsContext);
if (!context) {
throw new Error(`<${component}> must be rendered inside <Breadcrumbs>.`);
}
return context;
}
export interface BreadcrumbsProps extends HTMLAttributes<HTMLElement> {
/** Accessible label for the navigation landmark. */
label?: string;
/** Default separator content used by every `<BreadcrumbSeparator>` without children. */
separator?: ReactNode;
className?: string;
children?: ReactNode;
}
export function Breadcrumbs({
label = "Breadcrumb",
separator,
className,
children,
...rest
}: BreadcrumbsProps) {
return (
<BreadcrumbsContext.Provider value={{ separator: separator ?? DEFAULT_SEPARATOR }}>
<nav aria-label={label} className={className} {...rest}>
{children}
</nav>
</BreadcrumbsContext.Provider>
);
}
export interface BreadcrumbListProps extends HTMLAttributes<HTMLOListElement> {
className?: string;
children?: ReactNode;
}
export function BreadcrumbList({ className, children, ...rest }: BreadcrumbListProps) {
return (
<ol className={cx(LIST_CLASSES, className)} {...rest}>
{children}
</ol>
);
}
export interface BreadcrumbItemProps extends LiHTMLAttributes<HTMLLIElement> {
className?: string;
children?: ReactNode;
}
export function BreadcrumbItem({ className, children, ...rest }: BreadcrumbItemProps) {
return (
<li className={cx(ITEM_CLASSES, className)} {...rest}>
{children}
</li>
);
}
export interface BreadcrumbLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
/** Destination URL — rendered as a real anchor with normal browser navigation. */
href: string;
/** Meaningful leading icon (rendered aria-hidden). */
icon?: ReactNode;
className?: string;
children?: ReactNode;
}
export function BreadcrumbLink({ href, icon, className, children, title, ...rest }: BreadcrumbLinkProps) {
const fullLabel = title ?? (typeof children === "string" ? children : undefined);
return (
<a href={href} title={fullLabel} className={cx(LINK_CLASSES, className)} {...rest}>
{icon ? (
<span aria-hidden="true" className={ICON_CLASSES}>
{icon}
</span>
) : null}
<span className="min-w-0 truncate">{children}</span>
</a>
);
}
export interface BreadcrumbCurrentProps extends HTMLAttributes<HTMLSpanElement> {
/** Meaningful leading icon (rendered aria-hidden). */
icon?: ReactNode;
className?: string;
children?: ReactNode;
}
export function BreadcrumbCurrent({ icon, className, children, title, ...rest }: BreadcrumbCurrentProps) {
const fullLabel = title ?? (typeof children === "string" ? children : undefined);
return (
<span aria-current="page" title={fullLabel} className={cx(CURRENT_CLASSES, className)} {...rest}>
{icon ? (
<span aria-hidden="true" className={ICON_CLASSES}>
{icon}
</span>
) : null}
<span className="min-w-0 truncate">{children}</span>
</span>
);
}
export interface BreadcrumbSeparatorProps extends LiHTMLAttributes<HTMLLIElement> {
className?: string;
children?: ReactNode;
}
export function BreadcrumbSeparator({ className, children, ...rest }: BreadcrumbSeparatorProps) {
const context = useBreadcrumbs("BreadcrumbSeparator");
return (
<li
role="presentation"
aria-hidden="true"
className={cx(SEPARATOR_CLASSES, className)}
{...rest}
>
{children ?? context.separator}
</li>
);
}
export default Breadcrumbs; /* DevSnips React — JavaScript parity build.
* Same API, behavior, and classes as code.tsx; TypeScript types removed.
* Regenerated from code.tsx — edit code.tsx and re-run the generator.
*/
import { createContext, useContext } from "react";
function cx(...parts) {
return parts.filter(Boolean).join(" ");
}
const LIST_CLASSES = "m-0 flex max-w-full list-none flex-wrap items-center gap-x-1 gap-y-0.5 p-0 text-sm leading-5";
const ITEM_CLASSES = "inline-flex min-w-0 items-center gap-1.5";
const LINK_CLASSES = "inline-flex min-w-0 max-w-[9rem] items-center gap-1.5 rounded-[var(--ds-radius-xs)] text-[var(--ds-color-muted-foreground)] underline-offset-4 sm:max-w-[14rem] transition-colors duration-150 ease-out hover:text-[var(--ds-color-foreground)] hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const CURRENT_CLASSES = "inline-flex min-w-0 max-w-[9rem] items-center gap-1.5 font-medium text-[var(--ds-color-foreground)] sm:max-w-[14rem]";
const SEPARATOR_CLASSES = "inline-flex shrink-0 select-none items-center justify-center text-[var(--ds-color-muted-foreground)] [&_svg]:size-3.5";
const ICON_CLASSES = "inline-flex shrink-0 text-[14px] [&_svg]:size-3.5";
const DEFAULT_SEPARATOR = <svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="m9 6 6 6-6 6" />
</svg>;
const BreadcrumbsContext = createContext(null);
function useBreadcrumbs(component) {
const context = useContext(BreadcrumbsContext);
if (!context) {
throw new Error(`<${component}> must be rendered inside <Breadcrumbs>.`);
}
return context;
}
function Breadcrumbs({
label = "Breadcrumb",
separator,
className,
children,
...rest
}) {
return <BreadcrumbsContext.Provider value={{ separator: separator ?? DEFAULT_SEPARATOR }}>
<nav aria-label={label} className={className} {...rest}>
{children}
</nav>
</BreadcrumbsContext.Provider>;
}
function BreadcrumbList({ className, children, ...rest }) {
return <ol className={cx(LIST_CLASSES, className)} {...rest}>
{children}
</ol>;
}
function BreadcrumbItem({ className, children, ...rest }) {
return <li className={cx(ITEM_CLASSES, className)} {...rest}>
{children}
</li>;
}
function BreadcrumbLink({ href, icon, className, children, title, ...rest }) {
const fullLabel = title ?? (typeof children === "string" ? children : undefined);
return <a href={href} title={fullLabel} className={cx(LINK_CLASSES, className)} {...rest}>
{icon ? <span aria-hidden="true" className={ICON_CLASSES}>
{icon}
</span> : null}
<span className="min-w-0 truncate">{children}</span>
</a>;
}
function BreadcrumbCurrent({ icon, className, children, title, ...rest }) {
const fullLabel = title ?? (typeof children === "string" ? children : undefined);
return <span aria-current="page" title={fullLabel} className={cx(CURRENT_CLASSES, className)} {...rest}>
{icon ? <span aria-hidden="true" className={ICON_CLASSES}>
{icon}
</span> : null}
<span className="min-w-0 truncate">{children}</span>
</span>;
}
function BreadcrumbSeparator({ className, children, ...rest }) {
const context = useBreadcrumbs("BreadcrumbSeparator");
return <li
role="presentation"
aria-hidden="true"
className={cx(SEPARATOR_CLASSES, className)}
{...rest}
>
{children ?? context.separator}
</li>;
}
export { Breadcrumbs, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbCurrent, BreadcrumbSeparator };
export default Breadcrumbs; # Breadcrumbs Max Width
Breadcrumb trail that bounds long labels with max-width truncation while keeping the full text available through the title attribute — no clipped, meaningless navigation.
## Usage
```tsx
import Breadcrumbs, {
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbCurrent,
BreadcrumbSeparator,
} from "./breadcrumbs-max-width";
<Breadcrumbs>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/documentation/design-tokens">
Design tokens and theming guidelines
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbCurrent>Overriding tokens for white-label themes</BreadcrumbCurrent>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumbs>
// Labels truncate at 9rem (14rem from sm up). The `title` attribute is
// auto-filled from string children — pass `title` explicitly when the
// label is a ReactNode:
<BreadcrumbLink href="/glossary" title="White-label theming">
<em>White-label</em> theming
</BreadcrumbLink>
```
## JavaScript
A `code.jsx` build is provided for projects that ship plain JSX. It exposes the same API and behavior as `code.tsx` — only the TypeScript types are removed.
```jsx
import Breadcrumbs, {
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbCurrent,
BreadcrumbSeparator,
} from "./breadcrumbs-max-width";
<Breadcrumbs>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/documentation/design-tokens">
Design tokens and theming guidelines
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbCurrent>Overriding tokens for white-label themes</BreadcrumbCurrent>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumbs>
// Labels truncate at 9rem (14rem from sm up). The `title` attribute is
// auto-filled from string children — pass `title` explicitly when the
// label is a ReactNode:
<BreadcrumbLink href="/glossary" title="White-label theming">
<em>White-label</em> theming
</BreadcrumbLink>
```
## Props
### `<Breadcrumbs>`
| Name | Type | Default | Description |
|---|---|---|---|
| `label` | `string` | `"Breadcrumb"` | Accessible label for the `<nav>` landmark. |
| `separator` | `ReactNode` | chevron icon | Default separator content for every `<BreadcrumbSeparator>` without children. |
| `className` | `string` | — | Extra classes on the `<nav>`. |
| `children` | `ReactNode` | — | `BreadcrumbList` composition. |
### `<BreadcrumbList>`
| Name | Type | Default | Description |
|---|---|---|---|
| `className` | `string` | — | Extra classes on the `<ol>`. |
| `children` | `ReactNode` | — | `BreadcrumbItem` + `BreadcrumbSeparator` elements. |
### `<BreadcrumbItem>`
| Name | Type | Default | Description |
|---|---|---|---|
| `className` | `string` | — | Extra classes on the `<li>`. |
| `children` | `ReactNode` | — | Usually one `BreadcrumbLink` or `BreadcrumbCurrent`. |
### `<BreadcrumbLink>`
| Name | Type | Default | Description |
|---|---|---|---|
| `href` | `string` (required) | — | Destination URL — rendered as a real anchor with normal browser navigation. |
| `icon` | `ReactNode` | — | Meaningful leading icon (rendered `aria-hidden`). |
| `className` | `string` | — | Extra classes on the anchor. |
| `children` | `ReactNode` | — | Visible label. |
All native anchor attributes (`target`, `rel`, `aria-label`, `title`, …) are forwarded.
### `<BreadcrumbCurrent>`
| Name | Type | Default | Description |
|---|---|---|---|
| `icon` | `ReactNode` | — | Meaningful leading icon (rendered `aria-hidden`). |
| `className` | `string` | — | Extra classes on the span. |
| `children` | `ReactNode` | — | Visible label (rendered with `aria-current="page"`). |
### `<BreadcrumbSeparator>`
| Name | Type | Default | Description |
|---|---|---|---|
| `className` | `string` | — | Extra classes on the list item. |
| `children` | `ReactNode` | context `separator` | Custom separator content for this position only. |
## Composition
Breadcrumbs is a compound component. Six primitives compose the pattern:
```tsx
<Breadcrumbs>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/documentation">Documentation</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbCurrent>Buttons</BreadcrumbCurrent>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumbs>
```
- `Breadcrumbs` — the root `<nav aria-label="Breadcrumb">` landmark. Provides the default `separator` to every separator through context.
- `BreadcrumbList` — the ordered list (`<ol>`). Wraps onto multiple lines instead of scrolling horizontally.
- `BreadcrumbItem` — one level of the trail (`<li>`).
- `BreadcrumbLink` — a real `<a href>` for navigable levels: normal browser navigation, optional leading `icon`.
- `BreadcrumbCurrent` — the current page: plain text with `aria-current="page"`, never a link.
- `BreadcrumbSeparator` — decorative structure between levels (`role="presentation"`, `aria-hidden`); renders its own `children`, else the `separator` given to `<Breadcrumbs>`, else the default chevron.
Same six primitives — this variant bakes a bounded `max-w` + `truncate` into `BreadcrumbLink` and `BreadcrumbCurrent` and auto-fills `title` from string children, so truncation never destroys the meaning of the trail.
## Keyboard Interaction
| Key | Behavior |
|---|---|
| `Tab` / `Shift+Tab` | Move focus through the links |
| `Enter` | Follow the focused link |
## Accessibility
The structure follows the W3C Breadcrumb pattern: a `<nav aria-label="Breadcrumb">` landmark containing an ordered list (`<ol>`) whose last item is the current page marked with `aria-current="page"`.
- Every navigable level is a real `<a href>` — normal browser navigation, no click handlers faking links.
- The current page is plain text, not a link; assistive technology announces it as the current page.
- Separators are `aria-hidden` `role="presentation"` list items — never announced, never focusable.
- Icons are decorative (`aria-hidden`); the visible label always carries the accessible name.
Truncation is purely visual CSS (`truncate`) — the full label remains in the accessibility tree, so screen readers announce the complete text. The `title` attribute (auto-filled from string children, or passed explicitly for ReactNode labels) exposes the full text to sighted users on hover.
## States
- **Link** — muted foreground; hover shifts to the foreground color with an underline.
- **Current page** — foreground color at medium weight; not interactive.
- **Separator** — muted decorative glyph, hidden from assistive technology.
- **Focus-visible** — `--ds-color-focus-ring` outline on links and menu triggers in both themes.
- **Truncated label** — ends in an ellipsis inside `max-w-[9rem]` (`sm:max-w-[14rem]`); hovering reveals the native `title` tooltip with the full text.
## Responsive Behavior
Each label is capped at `max-w-[9rem]` below `sm` and `max-w-[14rem]` above, so no single verbose level can push the trail past the viewport. The list still wraps (`flex-wrap`) when several capped levels exceed the line — truncation bounds labels, wrapping handles volume. Neither creates page-level horizontal scrolling.
## Styling
Built with React, Tailwind CSS, and DevSnips design tokens. The component consumes the `--ds-*` semantic tokens via arbitrary values (for example `text-[var(--ds-color-muted-foreground)]`). Define the tokens once in your theme — no component-specific CSS file is required.
## Design Tokens
See [React/DESIGN_TOKENS.md](../../../DESIGN_TOKENS.md) for the authoritative token specification. This breadcrumb variant uses the semantic color, radius, typography, and motion tokens.
## Notes
Do not rely on `overflow-hidden` alone: without the `title` attribute and the full accessibility-tree text, clipping would destroy meaning. Tune the caps through the `LINK_CLASSES` / `CURRENT_CLASSES` constants if your density differs. 177 lines UTF-8 · LF · Spaces: 2
Continue browsing