shadimage
moduix-react
itembreadcrumbssource: ok · hit

Breadcrumbs

moduix React Breadcrumbs component.

npx shadcn add "https://moduix.dev/r/react/breadcrumbs.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@ark-ui/react

Registry dependencies

@moduix-react/a11y-labels@moduix-react/foundation@moduix-react/icons

Files3 · source cached 10/14/2026, 2:17:26 PM

src/components/breadcrumbs/Breadcrumbs.tsx
import type { HTMLArkProps } from '@ark-ui/react/factory';
import { ark } from '@ark-ui/react/factory';
import { clsx } from 'clsx';
import { Fragment, forwardRef, type ComponentRef, type ReactNode } from 'react';
import { a11yLabels } from '@/lib/moduix/a11yLabels';
import { ChevronRightIcon } from '@/lib/moduix/icons/ui';
import styles from './Breadcrumbs.module.css';

type BreadcrumbsPathLink = {
  href: string;
  label: ReactNode;
};
type BreadcrumbsPathProps = Omit<HTMLArkProps<'ol'>, 'asChild' | 'children'> & {
  links: readonly BreadcrumbsPathLink[];
  page: ReactNode;
  separator?: ReactNode;
};

const Breadcrumbs = forwardRef<ComponentRef<typeof ark.nav>, HTMLArkProps<'nav'>>(
  function Breadcrumbs(
    { className, 'aria-label': ariaLabel = a11yLabels.breadcrumb, ...props },
    ref,
  ) {
    return (
      <ark.nav
        ref={ref}
        aria-label={ariaLabel}
        {...props}
        data-slot="breadcrumbs-root"
        className={clsx(styles.root, className)}
      />
    );
  },
);

const BreadcrumbsList = forwardRef<ComponentRef<typeof ark.ol>, HTMLArkProps<'ol'>>(
  function BreadcrumbsList({ className, ...props }, ref) {
    return (
      <ark.ol
        ref={ref}
        {...props}
        data-slot="breadcrumbs-list"
        className={clsx(styles.list, className)}
      />
    );
  },
);

const BreadcrumbsItem = forwardRef<ComponentRef<typeof ark.li>, HTMLArkProps<'li'>>(
  function BreadcrumbsItem({ className, ...props }, ref) {
    return (
      <ark.li
        ref={ref}
        {...props}
        data-slot="breadcrumbs-item"
        className={clsx(styles.item, className)}
      />
    );
  },
);

const BreadcrumbsLink = forwardRef<ComponentRef<typeof ark.a>, HTMLArkProps<'a'>>(
  function BreadcrumbsLink({ className, ...props }, ref) {
    return (
      <ark.a
        ref={ref}
        {...props}
        data-slot="breadcrumbs-link"
        className={clsx(styles.link, className)}
      />
    );
  },
);

const BreadcrumbsPage = forwardRef<ComponentRef<typeof ark.span>, HTMLArkProps<'span'>>(
  function BreadcrumbsPage({ className, ...props }, ref) {
    return (
      <ark.span
        ref={ref}
        {...props}
        data-slot="breadcrumbs-page"
        aria-current="page"
        className={clsx(styles.page, className)}
      />
    );
  },
);

const BreadcrumbsPath = forwardRef<ComponentRef<typeof ark.ol>, BreadcrumbsPathProps>(
  function BreadcrumbsPath({ links, page, separator, ...props }, ref) {
    return (
      <BreadcrumbsList ref={ref} {...props}>
        {links.map((link, index) => (
          <Fragment key={index}>
            <BreadcrumbsItem>
              <BreadcrumbsLink href={link.href}>{link.label}</BreadcrumbsLink>
            </BreadcrumbsItem>
            <BreadcrumbsSeparator>{separator}</BreadcrumbsSeparator>
          </Fragment>
        ))}
        <BreadcrumbsItem>
          <BreadcrumbsPage>{page}</BreadcrumbsPage>
        </BreadcrumbsItem>
      </BreadcrumbsList>
    );
  },
);

const BreadcrumbsSeparator = forwardRef<ComponentRef<typeof ark.li>, HTMLArkProps<'li'>>(
  function BreadcrumbsSeparator({ className, children, ...props }, ref) {
    return (
      <ark.li
        ref={ref}
        {...props}
        data-slot="breadcrumbs-separator"
        aria-hidden="true"
        className={clsx(styles.separator, className)}
      >
        {children ?? <ChevronRightIcon className={styles.separatorIcon} />}
      </ark.li>
    );
  },
);

const BreadcrumbsEllipsis = forwardRef<ComponentRef<typeof ark.span>, HTMLArkProps<'span'>>(
  function BreadcrumbsEllipsis({ className, children = '...', ...props }, ref) {
    return (
      <ark.span
        ref={ref}
        {...props}
        data-slot="breadcrumbs-ellipsis"
        aria-hidden="true"
        className={clsx(styles.ellipsis, className)}
      >
        {children}
      </ark.span>
    );
  },
);

export {
  Breadcrumbs,
  BreadcrumbsEllipsis,
  BreadcrumbsItem,
  BreadcrumbsLink,
  BreadcrumbsList,
  BreadcrumbsPage,
  BreadcrumbsPath,
  BreadcrumbsSeparator,
};
src/components/breadcrumbs/Breadcrumbs.module.css
@layer moduix.components {
  .root {
    max-width: var(--moduix-breadcrumbs-max-width, 100%);
    color: var(--moduix-breadcrumbs-color, var(--moduix-color-muted-foreground));
    font-size: var(--moduix-breadcrumbs-font-size, var(--moduix-text-sm));
    line-height: var(--moduix-breadcrumbs-line-height, var(--moduix-line-height-text-sm));
  }

  .list {
    display: flex;
    min-width: 0;
    align-items: center;
    gap: var(--moduix-breadcrumbs-gap, var(--moduix-spacing-1));
    margin: 0;
    padding: 0;
    list-style: none;
    white-space: nowrap;

    > .item:last-child {
      flex: 1 1 auto;
      max-width: var(--moduix-breadcrumbs-item-max-width, 16rem);
    }
  }

  .item {
    display: inline-flex;
    min-width: 0;
    align-items: center;
  }

  .link,
  .page {
    display: block;
    min-width: 0;
    max-width: 100%;
    padding-inline: var(--moduix-breadcrumbs-item-padding-x, var(--moduix-spacing-1));
    white-space: nowrap;
  }

  .link,
  .page,
  .ellipsis {
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .link {
    color: var(--moduix-breadcrumbs-link-color, var(--moduix-color-muted-foreground));
    text-decoration: var(--moduix-breadcrumbs-link-text-decoration, none);
    text-underline-offset: var(--moduix-breadcrumbs-link-underline-offset, 0.2em);
    border-radius: var(--moduix-breadcrumbs-link-radius, var(--moduix-radius-sm));
    outline: none;
    transition: color var(--moduix-breadcrumbs-link-transition, var(--moduix-transition-default));

    @media (hover: hover) {
      &:hover {
        color: var(--moduix-breadcrumbs-link-color-hover, var(--moduix-color-foreground));
        text-decoration: var(--moduix-breadcrumbs-link-text-decoration-hover, none);
      }
    }

    &:focus-visible {
      box-shadow: 0 0 0
        var(
          --moduix-breadcrumbs-focus-ring-width,
          var(--moduix-focus-ring-inset-width, var(--moduix-border-width-sm))
        )
        var(--moduix-breadcrumbs-focus-ring-color, var(--moduix-color-ring));
    }
  }

  .page {
    color: var(--moduix-breadcrumbs-page-color, var(--moduix-color-foreground));
    font-weight: var(--moduix-breadcrumbs-page-font-weight, var(--moduix-weight-medium));
  }

  .separator {
    display: inline-flex;
    align-items: center;
    color: var(--moduix-breadcrumbs-separator-color, var(--moduix-color-muted-foreground));
    font-size: var(--moduix-breadcrumbs-separator-font-size, 0.875em);
    line-height: 1;
    user-select: none;
  }

  .separatorIcon {
    width: var(--moduix-breadcrumbs-separator-icon-size, 1em);
    height: var(--moduix-breadcrumbs-separator-icon-size, 1em);
    flex: none;
  }

  .separatorIcon:dir(rtl) {
    transform: rotate(180deg);
  }

  .ellipsis {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--moduix-breadcrumbs-ellipsis-size, var(--moduix-spacing-4));
    min-height: var(--moduix-breadcrumbs-ellipsis-size, var(--moduix-spacing-4));
    padding-inline: var(--moduix-breadcrumbs-item-padding-x, var(--moduix-spacing-1));
    color: var(--moduix-breadcrumbs-ellipsis-color, var(--moduix-color-muted-foreground));
  }
}
src/components/breadcrumbs/index.ts
export * from './Breadcrumbs.js';

More from moduix-react

Browse all components in this library →