Component
Breadcrumbs With Current
Breadcrumb trail that explicitly distinguishes the current location: aria-current page text that is never a navigable link, via BreadcrumbCurrent or the current prop for data-driven trails.
- Component
- React
- TSX
- Tailwind CSS
- MIT
Preview
Live component
9:41 100%
devsnips.dev/library/react/components/breadcrumbs/breadcrumbs-with-current/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add React/Components/Breadcrumbs/breadcrumbs-with-current React/Components/Breadcrumbs/breadcrumbs-with-current import { createContext, useContext } from "react";
import type {
AnchorHTMLAttributes,
HTMLAttributes,
LiHTMLAttributes,
ReactNode,
} from "react";
/**
* DevSnips React Breadcrumbs — With Current.
*
* The current page is explicitly distinguished: it carries
* `aria-current="page"` and is plain foreground text, never a navigable
* link. Compose it explicitly with `<BreadcrumbCurrent>`, or — when levels
* come from route data — pass `current` to `<BreadcrumbLink>` and the link
* renders as the non-navigable current indicator instead of an anchor, so
* data-driven trails can map every level through one component.
*/
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. Ignored when `current`. */
href: string;
/** Meaningful leading icon (rendered aria-hidden). */
icon?: ReactNode;
/** Marks this level as the current page: renders non-navigable text with `aria-current="page"` instead of an anchor. */
current?: boolean;
className?: string;
children?: ReactNode;
}
export function BreadcrumbLink({ href, icon, current, className, children, ...rest }: BreadcrumbLinkProps) {
if (current) {
return (
<span aria-current="page" className={cx(CURRENT_CLASSES, className)}>
{icon ? (
<span aria-hidden="true" className={ICON_CLASSES}>
{icon}
</span>
) : null}
<span className="min-w-0">{children}</span>
</span>
);
}
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, current, className, children, ...rest }) {
if (current) {
return <span aria-current="page" className={cx(CURRENT_CLASSES, className)}>
{icon ? <span aria-hidden="true" className={ICON_CLASSES}>
{icon}
</span> : null}
<span className="min-w-0">{children}</span>
</span>;
}
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 Current
Breadcrumb trail that explicitly distinguishes the current location: aria-current page text that is never a navigable link, via BreadcrumbCurrent or the current prop for data-driven trails.
## Usage
```tsx
import { Fragment } from "react";
import Breadcrumbs, {
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
} from "./breadcrumbs-with-current";
const trail = [
{ label: "Home", href: "/" },
{ label: "Documentation", href: "/documentation" },
{ label: "Components", href: "/documentation/components" },
{ label: "Tabs", href: "/documentation/components/tabs", current: true },
];
// Data-driven: `current` turns the link into non-navigable current text,
// so every level maps through one component without branching.
<Breadcrumbs>
<BreadcrumbList>
{trail.map((level, index) => (
<Fragment key={level.href}>
{index > 0 ? <BreadcrumbSeparator /> : null}
<BreadcrumbItem>
<BreadcrumbLink href={level.href} current={level.current}>
{level.label}
</BreadcrumbLink>
</BreadcrumbItem>
</Fragment>
))}
</BreadcrumbList>
</Breadcrumbs>
// Explicit composition uses <BreadcrumbCurrent> for the last level.
```
## 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 { Fragment } from "react";
import Breadcrumbs, {
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
} from "./breadcrumbs-with-current";
const trail = [
{ label: "Home", href: "/" },
{ label: "Documentation", href: "/documentation" },
{ label: "Components", href: "/documentation/components" },
{ label: "Tabs", href: "/documentation/components/tabs", current: true },
];
// Data-driven: `current` turns the link into non-navigable current text,
// so every level maps through one component without branching.
<Breadcrumbs>
<BreadcrumbList>
{trail.map((level, index) => (
<Fragment key={level.href}>
{index > 0 ? <BreadcrumbSeparator /> : null}
<BreadcrumbItem>
<BreadcrumbLink href={level.href} current={level.current}>
{level.label}
</BreadcrumbLink>
</BreadcrumbItem>
</Fragment>
))}
</BreadcrumbList>
</Breadcrumbs>
// Explicit composition uses <BreadcrumbCurrent> for the last level.
```
## 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.
Two ways to mark the current page: compose `<BreadcrumbCurrent>` explicitly, or pass `current` to `<BreadcrumbLink>` — it then renders the same non-navigable `aria-current="page"` text instead of an anchor, which keeps data-driven trails (route tables, CMS slugs) branch-free.
## 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.
The current page is never a link to itself: `aria-current="page"` marks it for assistive technology, and the medium-weight foreground treatment keeps the distinction subtle and token-driven — never color alone, since weight changes with it.
## 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
Use the `current` prop when levels come from data (every entry has an href, including the page you are on); use `<BreadcrumbCurrent>` when composing by hand. 189 lines UTF-8 · LF · Spaces: 2
Continue browsing