Component
Breadcrumbs With Separator
Breadcrumb trail with a configurable separator: set one separator node on the root for the whole trail, or override a single position — separators stay decorative and aria-hidden.
- Component
- React
- TSX
- Tailwind CSS
- MIT
Preview
Live component
9:41 100%
devsnips.dev/library/react/components/breadcrumbs/breadcrumbs-with-separator/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add React/Components/Breadcrumbs/breadcrumbs-with-separator React/Components/Breadcrumbs/breadcrumbs-with-separator import { createContext, useContext } from "react";
import type {
AnchorHTMLAttributes,
HTMLAttributes,
LiHTMLAttributes,
ReactNode,
} from "react";
/**
* DevSnips React Breadcrumbs — With Separator.
*
* The separator is structural decoration: each `<BreadcrumbSeparator>`
* renders an aria-hidden `role="presentation"` list item. Set one
* `separator` ReactNode on `<Breadcrumbs>` to restyle every separator at
* once, or pass children to an individual `<BreadcrumbSeparator>` to
* override it in place. The default is a small chevron.
*/
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 items-center gap-1.5 rounded-[var(--ds-radius-xs)] text-[var(--ds-color-muted-foreground)] underline-offset-4 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 items-center gap-1.5 font-medium text-[var(--ds-color-foreground)]";
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, ...rest }: BreadcrumbLinkProps) {
return (
<a href={href} className={cx(LINK_CLASSES, className)} {...rest}>
{icon ? (
<span aria-hidden="true" className={ICON_CLASSES}>
{icon}
</span>
) : null}
<span className="min-w-0">{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, ...rest }: BreadcrumbCurrentProps) {
return (
<span aria-current="page" className={cx(CURRENT_CLASSES, className)} {...rest}>
{icon ? (
<span aria-hidden="true" className={ICON_CLASSES}>
{icon}
</span>
) : null}
<span className="min-w-0">{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 items-center gap-1.5 rounded-[var(--ds-radius-xs)] text-[var(--ds-color-muted-foreground)] underline-offset-4 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 items-center gap-1.5 font-medium text-[var(--ds-color-foreground)]";
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, ...rest }) {
return <a href={href} className={cx(LINK_CLASSES, className)} {...rest}>
{icon ? <span aria-hidden="true" className={ICON_CLASSES}>
{icon}
</span> : null}
<span className="min-w-0">{children}</span>
</a>;
}
function BreadcrumbCurrent({ icon, className, children, ...rest }) {
return <span aria-current="page" className={cx(CURRENT_CLASSES, className)} {...rest}>
{icon ? <span aria-hidden="true" className={ICON_CLASSES}>
{icon}
</span> : null}
<span className="min-w-0">{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 With Separator
Breadcrumb trail with a configurable separator: set one separator node on the root for the whole trail, or override a single position — separators stay decorative and aria-hidden.
## Usage
```tsx
import Breadcrumbs, {
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbCurrent,
BreadcrumbSeparator,
} from "./breadcrumbs-with-separator";
// One separator for the whole trail:
<Breadcrumbs separator="/">
<BreadcrumbList>
<BreadcrumbItem><BreadcrumbLink href="/">Home</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbLink href="/documentation">Documentation</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbCurrent>Tabs</BreadcrumbCurrent></BreadcrumbItem>
</BreadcrumbList>
</Breadcrumbs>
// A custom icon separator:
<Breadcrumbs separator={<ChevronRightIcon />}>…</Breadcrumbs>
// Override a single position in place:
<BreadcrumbSeparator>{">"}</BreadcrumbSeparator>
```
## 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-with-separator";
// One separator for the whole trail:
<Breadcrumbs separator="/">
<BreadcrumbList>
<BreadcrumbItem><BreadcrumbLink href="/">Home</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbLink href="/documentation">Documentation</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbCurrent>Tabs</BreadcrumbCurrent></BreadcrumbItem>
</BreadcrumbList>
</Breadcrumbs>
// A custom icon separator:
<Breadcrumbs separator={<ChevronRightIcon />}>…</Breadcrumbs>
// Override a single position in place:
<BreadcrumbSeparator>{">"}</BreadcrumbSeparator>
```
## 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.
The separator is resolved per position: a `<BreadcrumbSeparator>` with children uses them, otherwise it falls back to the `separator` given to `<Breadcrumbs>` through context, otherwise the default chevron. Set it once on the root to restyle the whole 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.
Separators are structural decoration, not navigation: every separator renders an `aria-hidden` `role="presentation"` list item, so it is never announced, never focusable, and never behaves like a link — whichever glyph you choose.
## 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.
## Responsive Behavior
The list uses `flex-wrap` with a `min-w-0` flexible item per level, so long trails wrap onto multiple lines instead of forcing page-level horizontal scrolling. From 375px up, prefer intentional reduction over squeezing: collapse middle levels with `breadcrumbs-collapsed` and cap long labels with `breadcrumbs-max-width`.
## 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
Keep separators restrained: a chevron, a slash, or a single angle bracket. The glyph inherits the muted-foreground token and the 14px icon sizing, so custom separators stay in the same visual language. 174 lines UTF-8 · LF · Spaces: 2
Continue browsing