Breadcrumb

Icon-capable breadcrumb trail.

Trail

An icon-capable breadcrumb trail.

function BreadcrumbDemo() {
  return (
    <Breadcrumb>
      <BreadcrumbItem icon={RiHomeLine} href="#">
        Home
      </BreadcrumbItem>
      <BreadcrumbItem icon={RiFolderLine} href="#">
        Projects
      </BreadcrumbItem>
      <BreadcrumbItem>Design system</BreadcrumbItem>
    </Breadcrumb>
  );
}
function BreadcrumbDemo() {
  return (
    <Breadcrumb>
      <BreadcrumbItem icon={RiHomeLine} href="#">
        Home
      </BreadcrumbItem>
      <BreadcrumbItem icon={RiFolderLine} href="#">
        Projects
      </BreadcrumbItem>
      <BreadcrumbItem>Design system</BreadcrumbItem>
    </Breadcrumb>
  );
}

Installation

npx shadcn@latest add https://boardcn.dev/r/breadcrumb.json
npx shadcn@latest add https://boardcn.dev/r/breadcrumb.json

BoardCN dependencies

The CLI installs these for you — you do not need to add them yourself.

Source

The file the CLI copies into your project.

components/base/breadcrumb/breadcrumb.tsx
import { Children, Fragment, isValidElement } from "react";
import type { ComponentType, ReactNode, Ref } from "react";
import { ChevronRightSmall } from "@/components/foundations/icons/chevrons";
import { cx } from "@/utils/cx";

type IconComponent = ComponentType<{
  className?: string;
  "aria-hidden"?: boolean | "true" | "false";
}>;

export interface BreadcrumbProps {
  children: ReactNode;
  "aria-label"?: string;
  className?: string;
  ref?: Ref<HTMLElement>;
}

export function Breadcrumb({
  children,
  "aria-label": ariaLabel = "Breadcrumb",
  className,
  ref,
}: BreadcrumbProps) {
  const items = Children.toArray(children).filter(isValidElement);

  return (
    <nav
      ref={ref}
      aria-label={ariaLabel}
      className={cx(
        "flex w-full items-center overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
        className,
      )}
    >
      <ol className="flex items-center gap-2.5 px-1">
        {items.map((item, index) => (
          <Fragment key={item.key ?? index}>
            {index > 0 && (
              <ChevronRightSmall className="size-3 shrink-0 text-text-tertiary" aria-hidden />
            )}
            {item}
          </Fragment>
        ))}
      </ol>
    </nav>
  );
}

export interface BreadcrumbItemProps {
  children?: ReactNode;
  /** Optional leading icon (16px), inherits the item color. */
  icon?: IconComponent;
  href?: string;
  onClick?: () => void;
  /** Marks the active page: not interactive, darker text. */
  current?: boolean;
  className?: string;
}

export function BreadcrumbItem({
  children,
  icon: Icon,
  href,
  onClick,
  current = false,
  className,
}: BreadcrumbItemProps) {
  const content = (
    <>
      {Icon && <Icon className="size-4 shrink-0" aria-hidden />}
      {children}
    </>
  );

  if (current) {
    return (
      <li
        aria-current="page"
        className={cx(
          "flex items-center gap-1.5 text-caption-1-medium whitespace-nowrap text-text-secondary",
          className,
        )}
      >
        {content}
      </li>
    );
  }

  const interactiveClass = cx(
    "-mx-1 flex items-center gap-1.5 rounded-md px-1 py-0.5 text-caption-1-medium whitespace-nowrap text-text-tertiary",
    "transition-colors duration-150 ease outline-none",
    "hover:bg-background-primary-hover hover:text-text-secondary",
    "focus-visible:ring-2 focus-visible:ring-border-focus-ring",
    className,
  );

  return (
    <li className="flex items-center">
      {href ? (
        <a href={href} className={interactiveClass}>
          {content}
        </a>
      ) : (
        <button type="button" onClick={onClick} className={cx("cursor-pointer", interactiveClass)}>
          {content}
        </button>
      )}
    </li>
  );
}
import { Children, Fragment, isValidElement } from "react";
import type { ComponentType, ReactNode, Ref } from "react";
import { ChevronRightSmall } from "@/components/foundations/icons/chevrons";
import { cx } from "@/utils/cx";

type IconComponent = ComponentType<{
  className?: string;
  "aria-hidden"?: boolean | "true" | "false";
}>;

export interface BreadcrumbProps {
  children: ReactNode;
  "aria-label"?: string;
  className?: string;
  ref?: Ref<HTMLElement>;
}

export function Breadcrumb({
  children,
  "aria-label": ariaLabel = "Breadcrumb",
  className,
  ref,
}: BreadcrumbProps) {
  const items = Children.toArray(children).filter(isValidElement);

  return (
    <nav
      ref={ref}
      aria-label={ariaLabel}
      className={cx(
        "flex w-full items-center overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
        className,
      )}
    >
      <ol className="flex items-center gap-2.5 px-1">
        {items.map((item, index) => (
          <Fragment key={item.key ?? index}>
            {index > 0 && (
              <ChevronRightSmall className="size-3 shrink-0 text-text-tertiary" aria-hidden />
            )}
            {item}
          </Fragment>
        ))}
      </ol>
    </nav>
  );
}

export interface BreadcrumbItemProps {
  children?: ReactNode;
  /** Optional leading icon (16px), inherits the item color. */
  icon?: IconComponent;
  href?: string;
  onClick?: () => void;
  /** Marks the active page: not interactive, darker text. */
  current?: boolean;
  className?: string;
}

export function BreadcrumbItem({
  children,
  icon: Icon,
  href,
  onClick,
  current = false,
  className,
}: BreadcrumbItemProps) {
  const content = (
    <>
      {Icon && <Icon className="size-4 shrink-0" aria-hidden />}
      {children}
    </>
  );

  if (current) {
    return (
      <li
        aria-current="page"
        className={cx(
          "flex items-center gap-1.5 text-caption-1-medium whitespace-nowrap text-text-secondary",
          className,
        )}
      >
        {content}
      </li>
    );
  }

  const interactiveClass = cx(
    "-mx-1 flex items-center gap-1.5 rounded-md px-1 py-0.5 text-caption-1-medium whitespace-nowrap text-text-tertiary",
    "transition-colors duration-150 ease outline-none",
    "hover:bg-background-primary-hover hover:text-text-secondary",
    "focus-visible:ring-2 focus-visible:ring-border-focus-ring",
    className,
  );

  return (
    <li className="flex items-center">
      {href ? (
        <a href={href} className={interactiveClass}>
          {content}
        </a>
      ) : (
        <button type="button" onClick={onClick} className={cx("cursor-pointer", interactiveClass)}>
          {content}
        </button>
      )}
    </li>
  );
}

Props

Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.

Breadcrumb

PropTypeDefaultDescription
aria-labelstringBreadcrumb
classNamestring

BreadcrumbItem

PropTypeDefaultDescription
classNamestring
currentbooleanfalseMarks the active page: not interactive, darker text.
hrefstring
iconIconComponentOptional leading icon (16px), inherits the item color.
onClick() => void