Glrk UI

Breadcrumb

A simplified wrapper for Shadcn Breadcrumb with flexible option handling and automatic type conversion

Installation

npx shadcn@latest add @glrk-ui/breadcrumb

If you haven't set up the prerequisites yet, check out Prerequest section.

Copy and paste the following code into shadcn breadcrumb component.

import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
} from "@/components/ui/dropdown-menu"
ui/breadcrumb.tsx
type breadcrumbItemT = string | {
  label: React.ReactNode
  href?: string
  onClick?: () => void
  className?: string
}

type breadcrumbItemsT = breadcrumbItemT[]

type base = {
  itemCls?: string
  linkCls?: string
  pageCls?: string
}

type listBase = base & {
  separator?: React.ReactNode
  separatorCls?: string
}

type itemProps = base & {
  item: breadcrumbItemT
  isLast: boolean
}
function Item({
  item: itemObj,
  isLast,
  itemCls,
  linkCls,
  pageCls,
}: itemProps) {
  const item = typeof itemObj === "string" ? { label: itemObj } : itemObj

  return (
    <BreadcrumbItem className={cn(itemCls, item?.className)}>
      {isLast
        ? <BreadcrumbPage className={pageCls}>{item?.label}</BreadcrumbPage>
        : Object?.keys(item)?.length > 2
          ? <BreadcrumbLink
            {...item}
            className={cn("cursor-pointer", linkCls)}
          >
            {item?.label}
          </BreadcrumbLink>
          : <span className={linkCls}>{item?.label}</span>
      }
    </BreadcrumbItem>
  )
}

type fullItemsProps = listBase & {
  items: breadcrumbItemsT
}
function BreadcrumbFullItems({
  items,
  separator,
  separatorCls,
  ...rest
}: fullItemsProps) {
  return items.map((item, index) => {
    const isLast = index === items.length - 1

    return (
      <React.Fragment key={index}>
        <Item
          item={item}
          isLast={isLast}
          {...rest}
        />
        {!isLast && (
          <BreadcrumbSeparator className={separatorCls}>
            {separator}
          </BreadcrumbSeparator>
        )}
      </React.Fragment>
    )
  })
}

type dropdownProps = listBase & {
  hiddenItems: breadcrumbItemsT
  dropdownCls?: string
}
function BreadcrumbDropdown({
  hiddenItems,
  dropdownCls,
}: dropdownProps) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger>
        <BreadcrumbEllipsis />
      </DropdownMenuTrigger>

      <DropdownMenuContent className={dropdownCls}>
        {hiddenItems.map((itemObj, index) => {
          const item = typeof itemObj === "string" ? { label: itemObj } : itemObj
          return (
            <DropdownMenuItem asChild key={`hidden-${index}`}>
              {item?.href ? (
                <a href={item?.href} className="w-full">{item?.label}</a>
              ) : item?.onClick ? (
                <button onClick={item?.onClick} className="w-full text-left">
                  {item?.label}
                </button>
              ) : (
                <span>{item?.label}</span>
              )}
            </DropdownMenuItem>
          )
        })}
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

type wrapperProps = listBase & {
  items: breadcrumbItemsT
  maxItems?: number
  itemsBeforeCollapse?: number
  itemsAfterCollapse?: number
  collapseType?: "ellipsis" | "dropdown"
  containerCls?: string
  dropdownCls?: string
  listCls?: string
}
function BreadcrumbWrapper({
  items,
  maxItems = 4,
  itemsBeforeCollapse = 1,
  itemsAfterCollapse = 1,
  collapseType = "ellipsis",
  containerCls,
  listCls,
  dropdownCls,
  separator,
  separatorCls,
  ...rest
}: wrapperProps) {
  const shouldCollapse = maxItems && items.length > maxItems

  if (!shouldCollapse) {
    return (
      <Breadcrumb className={containerCls}>
        <BreadcrumbList className={listCls}>
          <BreadcrumbFullItems
            items={items}
            separator={separator}
            separatorCls={separatorCls}
            {...rest}
          />
        </BreadcrumbList>
      </Breadcrumb>
    )
  }

  const startItems = items.slice(0, itemsBeforeCollapse)
  const endItems = items.slice(-itemsAfterCollapse)
  const hiddenItems = items.slice(itemsBeforeCollapse, -itemsAfterCollapse)

  return (
    <Breadcrumb className={containerCls}>
      <BreadcrumbList className={listCls}>
        {startItems.map((item, index) => (
          <React.Fragment key={`start-${index}`}>
            <Item
              item={item}
              isLast={false}
              {...rest}
            />
            <BreadcrumbSeparator className={separatorCls}>
              {separator}
            </BreadcrumbSeparator>
          </React.Fragment>
        ))}

        <BreadcrumbItem>
          {
            collapseType === "ellipsis"
              ? <BreadcrumbEllipsis />
              : <BreadcrumbDropdown
                hiddenItems={hiddenItems}
                dropdownCls={dropdownCls}
              />
          }
        </BreadcrumbItem>

        {endItems.map((item, index) => {
          const isLast = index === endItems.length - 1

          return (
            <React.Fragment key={`end-${index}`}>
              <BreadcrumbSeparator className={separatorCls}>
                {separator}
              </BreadcrumbSeparator>
              <Item
                item={item}
                isLast={isLast}
                {...rest}
              />
            </React.Fragment>
          )
        })}
      </BreadcrumbList>
    </Breadcrumb>
  )
}

Add export statements.

export {
  Breadcrumb,
  BreadcrumbList,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbPage,
  BreadcrumbSeparator,
  BreadcrumbEllipsis,
  BreadcrumbWrapper,
  type breadcrumbItemT,
  type breadcrumbItemsT,
}

Add dropdown-menu from shadcn.

Or use

npx shadcn@latest add dropdown-menu

Copy and paste the following code into your project.

ui/breadcrumb.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"

import { cn } from "@/lib/utils"

import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
} from "@/components/ui/dropdown-menu"

function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
  return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}

function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
  return (
    <ol
      data-slot="breadcrumb-list"
      className={cn(
        "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
        className
      )}
      {...props}
    />
  )
}

function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
  return (
    <li
      data-slot="breadcrumb-item"
      className={cn("inline-flex items-center gap-1.5", className)}
      {...props}
    />
  )
}

function BreadcrumbLink({
  asChild,
  className,
  ...props
}: React.ComponentProps<"a"> & {
  asChild?: boolean
}) {
  const Comp = asChild ? Slot : "a"

  return (
    <Comp
      data-slot="breadcrumb-link"
      className={cn("hover:text-foreground transition-colors", className)}
      {...props}
    />
  )
}

function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="breadcrumb-page"
      role="link"
      aria-disabled="true"
      aria-current="page"
      className={cn("text-foreground font-normal", className)}
      {...props}
    />
  )
}

function BreadcrumbSeparator({
  children,
  className,
  ...props
}: React.ComponentProps<"li">) {
  return (
    <li
      data-slot="breadcrumb-separator"
      role="presentation"
      aria-hidden="true"
      className={cn("[&>svg]:size-3.5", className)}
      {...props}
    >
      {children ?? <ChevronRight />}
    </li>
  )
}

function BreadcrumbEllipsis({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="breadcrumb-ellipsis"
      role="presentation"
      aria-hidden="true"
      className={cn("flex size-9 items-center justify-center", className)}
      {...props}
    >
      <MoreHorizontal className="size-4" />
      <span className="sr-only">More</span>
    </span>
  )
}

type breadcrumbItemT = string | {
  label: React.ReactNode
  href?: string
  onClick?: () => void
  className?: string
}

type breadcrumbItemsT = breadcrumbItemT[]

type base = {
  itemCls?: string
  linkCls?: string
  pageCls?: string
}

type listBase = base & {
  separator?: React.ReactNode
  separatorCls?: string
}

type itemProps = base & {
  item: breadcrumbItemT
  isLast: boolean
}
function Item({
  item: itemObj,
  isLast,
  itemCls,
  linkCls,
  pageCls,
}: itemProps) {
  const item = typeof itemObj === "string" ? { label: itemObj } : itemObj

  return (
    <BreadcrumbItem className={cn(itemCls, item?.className)}>
      {isLast
        ? <BreadcrumbPage className={pageCls}>{item?.label}</BreadcrumbPage>
        : Object?.keys(item)?.length > 2
          ? <BreadcrumbLink
            {...item}
            className={cn("cursor-pointer", linkCls)}
          >
            {item?.label}
          </BreadcrumbLink>
          : <span className={linkCls}>{item?.label}</span>
      }
    </BreadcrumbItem>
  )
}

type fullItemsProps = listBase & {
  items: breadcrumbItemsT
}
function BreadcrumbFullItems({
  items,
  separator,
  separatorCls,
  ...rest
}: fullItemsProps) {
  return items.map((item, index) => {
    const isLast = index === items.length - 1

    return (
      <React.Fragment key={index}>
        <Item
          item={item}
          isLast={isLast}
          {...rest}
        />
        {!isLast && (
          <BreadcrumbSeparator className={separatorCls}>
            {separator}
          </BreadcrumbSeparator>
        )}
      </React.Fragment>
    )
  })
}

type dropdownProps = listBase & {
  hiddenItems: breadcrumbItemsT
  dropdownCls?: string
}
function BreadcrumbDropdown({
  hiddenItems,
  dropdownCls,
}: dropdownProps) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger>
        <BreadcrumbEllipsis />
      </DropdownMenuTrigger>

      <DropdownMenuContent className={dropdownCls}>
        {hiddenItems.map((itemObj, index) => {
          const item = typeof itemObj === "string" ? { label: itemObj } : itemObj
          return (
            <DropdownMenuItem asChild key={`hidden-${index}`}>
              {item?.href ? (
                <a href={item?.href} className="w-full">{item?.label}</a>
              ) : item?.onClick ? (
                <button onClick={item?.onClick} className="w-full text-left">
                  {item?.label}
                </button>
              ) : (
                <span>{item?.label}</span>
              )}
            </DropdownMenuItem>
          )
        })}
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

type wrapperProps = listBase & {
  items: breadcrumbItemsT
  maxItems?: number
  itemsBeforeCollapse?: number
  itemsAfterCollapse?: number
  collapseType?: "ellipsis" | "dropdown"
  containerCls?: string
  dropdownCls?: string
  listCls?: string
}
function BreadcrumbWrapper({
  items,
  maxItems = 4,
  itemsBeforeCollapse = 1,
  itemsAfterCollapse = 1,
  collapseType = "ellipsis",
  containerCls,
  listCls,
  dropdownCls,
  separator,
  separatorCls,
  ...rest
}: wrapperProps) {
  const shouldCollapse = maxItems && items.length > maxItems

  if (!shouldCollapse) {
    return (
      <Breadcrumb className={containerCls}>
        <BreadcrumbList className={listCls}>
          <BreadcrumbFullItems
            items={items}
            separator={separator}
            separatorCls={separatorCls}
            {...rest}
          />
        </BreadcrumbList>
      </Breadcrumb>
    )
  }

  const startItems = items.slice(0, itemsBeforeCollapse)
  const endItems = items.slice(-itemsAfterCollapse)
  const hiddenItems = items.slice(itemsBeforeCollapse, -itemsAfterCollapse)

  return (
    <Breadcrumb className={containerCls}>
      <BreadcrumbList className={listCls}>
        {startItems.map((item, index) => (
          <React.Fragment key={`start-${index}`}>
            <Item
              item={item}
              isLast={false}
              {...rest}
            />
            <BreadcrumbSeparator className={separatorCls}>
              {separator}
            </BreadcrumbSeparator>
          </React.Fragment>
        ))}

        <BreadcrumbItem>
          {
            collapseType === "ellipsis"
              ? <BreadcrumbEllipsis />
              : <BreadcrumbDropdown
                hiddenItems={hiddenItems}
                dropdownCls={dropdownCls}
              />
          }
        </BreadcrumbItem>

        {endItems.map((item, index) => {
          const isLast = index === endItems.length - 1

          return (
            <React.Fragment key={`end-${index}`}>
              <BreadcrumbSeparator className={separatorCls}>
                {separator}
              </BreadcrumbSeparator>
              <Item
                item={item}
                isLast={isLast}
                {...rest}
              />
            </React.Fragment>
          )
        })}
      </BreadcrumbList>
    </Breadcrumb>
  )
}

export {
  Breadcrumb,
  BreadcrumbList,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbPage,
  BreadcrumbSeparator,
  BreadcrumbEllipsis,
  BreadcrumbWrapper,
  type breadcrumbItemT,
  type breadcrumbItemsT,
}

Done

You can now use BreadcrumbWrapper

Usage

Basic

import { BreadcrumbWrapper } from "@/components/ui/accordion";

export function Basic() {
  return (
    <BreadcrumbWrapper
      items={["Store", "Products", "Electronics", "Computers"]}
    />
  )
}

Advanced

import { BreadcrumbWrapper } from "@/components/ui/accordion";

export function Basic() {
  return (
    <BreadcrumbWrapper
      items={[
        { label: "Home", href: "/" },
        { label: "Products", href: "/products" },
        { label: "Category", href: "/products/category" },
        { label: "Electronics", href: "/products/category/electronics" },
        { label: "Laptops", href: "/products/category/electronics/laptops" },
        { label: "MacBook Air M3" }
      ]}
      collapseType="dropdown"
      itemsAfterCollapse={2}
    />
  )
}

Reference

Prop

Type

Prop

Type