Breadcrumb

Displays the path to the current resource using a hierarchy of links.

import {
  Breadcrumb,
  BreadcrumbEllipsis,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export default function BreadcrumbDemo() {
  return (
    <Breadcrumb>
      <BreadcrumbList>
        <BreadcrumbItem>
          <BreadcrumbLink href="#">Home</BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <DropdownMenu>
            <DropdownMenuTrigger
              render={<Button variant="ghost" size="icon-sm" aria-label="Toggle menu" />}
            >
              <BreadcrumbEllipsis />
            </DropdownMenuTrigger>
            <DropdownMenuContent align="start">
              <DropdownMenuGroup>
                <DropdownMenuItem>Documentation</DropdownMenuItem>
                <DropdownMenuItem>Themes</DropdownMenuItem>
                <DropdownMenuItem>GitHub</DropdownMenuItem>
              </DropdownMenuGroup>
            </DropdownMenuContent>
          </DropdownMenu>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <BreadcrumbLink href="#">Components</BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <BreadcrumbPage>Breadcrumb</BreadcrumbPage>
        </BreadcrumbItem>
      </BreadcrumbList>
    </Breadcrumb>
  )
}

Installation

Install the following dependencies:

pnpm add @base-ui/react @stylexjs/stylex clsx lucide-react

Set up the token layer and the StyleX compiler, if you have not already. See Installation. The breadcrumb also needs interop.css, which sizes the separator and ellipsis icons.

Copy and paste the following code into your project.

components/ui/breadcrumb.tsx
"use client"

import { useRender } from "@base-ui/react/use-render"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"
import { clsx } from "clsx"
import { ChevronRight, MoreHorizontal } from "lucide-react"

import { color, duration, ease, leading, radius, space, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  list: {
    display: "flex",
    flexWrap: "wrap",
    alignItems: "center",
    columnGap: space.s1_5,
    rowGap: space.s1_5,
    listStyleType: "none",
    overflowWrap: "break-word",
    color: color.mutedForeground,
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  item: { display: "inline-flex", alignItems: "center", gap: space.s1 },
  link: {
    borderRadius: radius.sm,
    color: { default: null, ":hover": color.foreground },
    outlineColor: color.ring,
    outlineWidth: 2,
    outlineOffset: 2,
    outlineStyle: { default: "none", ":focus-visible": "solid" },
    transitionProperty: "color",
    transitionDuration: duration.color,
    transitionTimingFunction: ease.standard,
  },
  page: { color: color.foreground, fontWeight: 400 },
  separator: {
    display: "inline-flex",
    alignItems: "center",
    "--carbon-icon-size": space.s3_5,
  },
  ellipsis: {
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    width: space.s5,
    height: space.s5,
    "--carbon-icon-size": space.s4,
  },
})

const breadcrumbStyles = {
  list: styles.list,
  item: styles.item,
  link: styles.link,
  page: styles.page,
  separator: styles.separator,
  ellipsis: styles.ellipsis,
}

type Overrides = { xstyle?: StyleXStyles; className?: string }

function Breadcrumb({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"nav"> & Overrides) {
  const sx = stylex.props(xstyle)
  return useRender({
    defaultTagName: "nav",
    render,
    ref,
    props: {
      "aria-label": "breadcrumb",
      "data-slot": "breadcrumb",
      ...sx,
      ...props,
      className: clsx(sx.className, className),
      style: { ...sx.style, ...style },
    },
  })
}

function BreadcrumbList({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"ol"> & Overrides) {
  const sx = stylex.props(styles.list, xstyle)
  return useRender({
    defaultTagName: "ol",
    render,
    ref,
    props: {
      "data-slot": "breadcrumb-list",
      ...sx,
      ...props,
      className: clsx(sx.className, className),
      style: { ...sx.style, ...style },
    },
  })
}

function BreadcrumbItem({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"li"> & Overrides) {
  const sx = stylex.props(styles.item, xstyle)
  return useRender({
    defaultTagName: "li",
    render,
    ref,
    props: {
      "data-slot": "breadcrumb-item",
      ...sx,
      ...props,
      className: clsx(sx.className, className),
      style: { ...sx.style, ...style },
    },
  })
}

type BreadcrumbLinkProps = useRender.ComponentProps<"a"> & { xstyle?: StyleXStyles }

function BreadcrumbLink({ xstyle, className, render, ...props }: BreadcrumbLinkProps) {
  const sx = stylex.props(styles.link, xstyle)
  return useRender({
    defaultTagName: "a",
    render,
    props: {
      "data-slot": "breadcrumb-link",
      ...sx,
      ...props,
      className: clsx(sx.className, className),
    },
  })
}

function BreadcrumbPage({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"span"> & Overrides) {
  const sx = stylex.props(styles.page, xstyle)
  return useRender({
    defaultTagName: "span",
    render,
    ref,
    props: {
      "data-slot": "breadcrumb-page",
      role: "link",
      "aria-disabled": "true",
      "aria-current": "page",
      ...sx,
      ...props,
      className: clsx(sx.className, className),
      style: { ...sx.style, ...style },
    },
  })
}

function BreadcrumbSeparator({
  render,
  ref,
  style,
  children,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"li"> & Overrides) {
  const sx = stylex.props(styles.separator, xstyle)
  return useRender({
    defaultTagName: "li",
    render,
    ref,
    props: {
      "data-slot": "breadcrumb-separator",
      role: "presentation",
      "aria-hidden": "true",
      ...sx,
      ...props,
      className: clsx("carbon-icons", sx.className, className),
      style: { ...sx.style, ...style },
      children: <>{children ?? <ChevronRight />}</>,
    },
  })
}

function BreadcrumbEllipsis({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"span"> & Overrides) {
  const sx = stylex.props(styles.ellipsis, xstyle)
  return useRender({
    defaultTagName: "span",
    render,
    ref,
    props: {
      "data-slot": "breadcrumb-ellipsis",
      role: "presentation",
      "aria-hidden": "true",
      ...sx,
      ...props,
      className: clsx("carbon-icons", sx.className, className),
      style: { ...sx.style, ...style },
      children: (
        <>
          <MoreHorizontal />
        </>
      ),
    },
  })
}

export {
  Breadcrumb,
  BreadcrumbEllipsis,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
  breadcrumbStyles,
}

Update the import paths to match your project setup.

Usage

import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink href="/">Home</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>Breadcrumb</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>

Composition

Breadcrumb
└── BreadcrumbList
    ├── BreadcrumbItem
    │   └── BreadcrumbLink
    ├── BreadcrumbSeparator
    └── BreadcrumbItem
        └── BreadcrumbPage

Basic

A trail of links ending in the current page.

import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"

export default function BreadcrumbBasic() {
  return (
    <Breadcrumb>
      <BreadcrumbList>
        <BreadcrumbItem>
          <BreadcrumbLink href="#">Home</BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <BreadcrumbLink href="#">Components</BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <BreadcrumbPage>Breadcrumb</BreadcrumbPage>
        </BreadcrumbItem>
      </BreadcrumbList>
    </Breadcrumb>
  )
}

Custom separator

Pass children to BreadcrumbSeparator to replace the chevron.

import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Slash } from "lucide-react"

export default function BreadcrumbSeparatorDemo() {
  return (
    <Breadcrumb>
      <BreadcrumbList>
        <BreadcrumbItem>
          <BreadcrumbLink href="#">Home</BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator>
          <Slash />
        </BreadcrumbSeparator>
        <BreadcrumbItem>
          <BreadcrumbLink href="#">Components</BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator>
          <Slash />
        </BreadcrumbSeparator>
        <BreadcrumbItem>
          <BreadcrumbPage>Breadcrumb</BreadcrumbPage>
        </BreadcrumbItem>
      </BreadcrumbList>
    </Breadcrumb>
  )
}

Put a DropdownMenu inside a BreadcrumbItem to branch off a segment.

import * as stylex from "@stylexjs/stylex"
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { color, space } from "@/theme/tokens.stylex"
import { ChevronDown } from "lucide-react"

const styles = stylex.create({
  trigger: {
    display: "inline-flex",
    alignItems: "center",
    gap: space.s1,
    color: { default: null, ":hover": color.foreground },
  },
  chevron: { width: space.s3_5, height: space.s3_5 },
})

export default function BreadcrumbDropdown() {
  return (
    <Breadcrumb>
      <BreadcrumbList>
        <BreadcrumbItem>
          <BreadcrumbLink href="#">Home</BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <DropdownMenu>
            <DropdownMenuTrigger {...stylex.props(styles.trigger)}>
              Components
              <ChevronDown {...stylex.props(styles.chevron)} />
            </DropdownMenuTrigger>
            <DropdownMenuContent align="start">
              <DropdownMenuGroup>
                <DropdownMenuItem>Documentation</DropdownMenuItem>
                <DropdownMenuItem>Themes</DropdownMenuItem>
                <DropdownMenuItem>GitHub</DropdownMenuItem>
              </DropdownMenuGroup>
            </DropdownMenuContent>
          </DropdownMenu>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <BreadcrumbPage>Breadcrumb</BreadcrumbPage>
        </BreadcrumbItem>
      </BreadcrumbList>
    </Breadcrumb>
  )
}

Collapsed

Use BreadcrumbEllipsis to stand in for the segments a long trail hides.

import {
  Breadcrumb,
  BreadcrumbEllipsis,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"

export default function BreadcrumbEllipsisDemo() {
  return (
    <Breadcrumb>
      <BreadcrumbList>
        <BreadcrumbItem>
          <BreadcrumbLink href="#">Home</BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <BreadcrumbEllipsis />
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <BreadcrumbLink href="#">Components</BreadcrumbLink>
        </BreadcrumbItem>
        <BreadcrumbSeparator />
        <BreadcrumbItem>
          <BreadcrumbPage>Breadcrumb</BreadcrumbPage>
        </BreadcrumbItem>
      </BreadcrumbList>
    </Breadcrumb>
  )
}

BreadcrumbEllipsis is aria-hidden and has no accessible name. When it triggers a menu, put an aria-label on the control around it.

Pass a router link through render on BreadcrumbLink. BreadcrumbLink then applies its styles and props to your link instead of an a.

import { Link } from "@tanstack/react-router"
<BreadcrumbLink render={<Link to="/docs" />}>Docs</BreadcrumbLink>

API reference

Pass render to replace a part's element or compose it with another component.

Every part takes xstyle and className.

The nav landmark. It renders aria-label="breadcrumb" and carries no styling of its own, so a list can be rendered without it.

Renders an a and accepts its props.

PropTypeDefault
renderReact.ReactNode<a />

Renders an li marked role="presentation".

PropTypeDefault
childrenReact.ReactNode<ChevronRight />

The style map behind the component, exported so the same look can go on any element.

KeyDescription
listThe wrapping row, its type and colour
itemOne segment's inline box
linkThe hover colour and the focus ring
pageThe current segment
separatorThe separator's icon size
ellipsisThe ellipsis box and its icon size

Data attributes

AttributeOnEffect
data-slot="breadcrumb"the navTargets the landmark from a parent
data-slot="breadcrumb-list"the listTargets the row
data-slot="breadcrumb-item"an itemTargets one segment
data-slot="breadcrumb-link"a linkPresent on whatever render produces
data-slot="breadcrumb-page"the last itemAlso carries aria-current="page"
data-slot="breadcrumb-separator"the separatorAlso role="presentation" and aria-hidden
data-slot="breadcrumb-ellipsis"the ellipsisAlso role="presentation" and aria-hidden

The separator and the ellipsis carry the carbon-icons class and set --carbon-icon-size, which is how interop.css sizes the icon inside each.