BreadCrumb
Responsive breadcrumb wrapper with collapsed overflow items.
hardening
components/ui/breadcrumb.tsx
Hardening
This component includes router integration. Prefer custom render links when integrating outside TanStack Router contexts.
Recipe Source
"use client";import { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator,} from "@giddaa-housing/ui/breadcrumb";import { Button } from "@giddaa-housing/ui/button";import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger,} from "@giddaa-housing/ui/dropdown-menu";import type { LinkProps } from "@tanstack/react-router";import { Link, useNavigate } from "@tanstack/react-router";import { Fragment } from "react";import { cn } from "./lib/cn";import { useIsMobile } from "./lib/use-mobile";type BreadCrumbItem = { label: React.ReactNode; to?: LinkProps["to"]; params?: LinkProps["params"]; search?: LinkProps["search"]; /** * Optional element to render the crumb link as (e.g. a custom `<Link>`). * Falls back to a router `<Link>` built from `to`/`params`/`search`. */ render?: React.ReactElement;};type BreadCrumbEllipsisItem = { kind: "ellipsis"; items: BreadCrumbItem[];};type BreadCrumbDisplayItem = BreadCrumbItem | BreadCrumbEllipsisItem;type BreadCrumbProps = { items: BreadCrumbItem[]; /** Max crumbs shown inline on small screens before collapsing the middle. */ maxVisibleMobile?: number; /** Max crumbs shown inline on larger screens before collapsing the middle. */ maxVisibleDesktop?: number; /** Leading crumbs always kept before the ellipsis. */ itemsBeforeCollapse?: number; className?: string; listClassName?: string;};const getDisplayItems = ( items: BreadCrumbItem[], maxVisible: number, itemsBeforeCollapse: number,): BreadCrumbDisplayItem[] => { if (items.length <= maxVisible || maxVisible < 2) { return items; } const leadCount = Math.min(Math.max(itemsBeforeCollapse, 0), maxVisible - 1); const trailCount = Math.max(maxVisible - leadCount, 1); const leading = items.slice(0, leadCount); const trailing = items.slice(items.length - trailCount); const hidden = items.slice(leadCount, items.length - trailCount); if (!hidden.length) { return items; } return [...leading, { kind: "ellipsis", items: hidden }, ...trailing];};function BreadCrumb({ items, maxVisibleMobile = 2, maxVisibleDesktop = 4, itemsBeforeCollapse = 1, className, listClassName,}: BreadCrumbProps) { const isMobile = useIsMobile(); const navigate = useNavigate(); if (!items.length) { return null; } const maxVisible = isMobile ? maxVisibleMobile : maxVisibleDesktop; const displayItems = getDisplayItems(items, maxVisible, itemsBeforeCollapse); return ( <Breadcrumb className={cn("overflow-x-auto", className)}> <BreadcrumbList className={cn( "flex-nowrap whitespace-nowrap text-[12px]", listClassName, )} > {displayItems.map((item, index) => { const isEllipsis = "kind" in item; const isLastItem = index === displayItems.length - 1; const key = isEllipsis ? `ellipsis-${index}` : `item-${item.to ?? index}`; return ( <Fragment key={key}> <BreadcrumbItem> {isEllipsis ? ( <DropdownMenu> <DropdownMenuTrigger render={ <Button size="icon-sm" type="button" variant="ghost" /> } > <BreadcrumbEllipsis /> <span className="sr-only">Toggle breadcrumb menu</span> </DropdownMenuTrigger> <DropdownMenuContent align="start" className="min-w-fit sm:max-w-xs" > <DropdownMenuGroup> {item.items.map((hiddenItem, hiddenIndex) => ( <DropdownMenuItem key={`hidden-${hiddenItem.to ?? hiddenIndex}`} disabled={!hiddenItem.to && !hiddenItem.render} render={hiddenItem.render} onClick={() => { if (!hiddenItem.to) { return; } void navigate({ to: hiddenItem.to, params: hiddenItem.params, search: hiddenItem.search, }); }} > {hiddenItem.label} </DropdownMenuItem> ))} </DropdownMenuGroup> </DropdownMenuContent> </DropdownMenu> ) : isLastItem ? ( <BreadcrumbPage>{item.label}</BreadcrumbPage> ) : item.render ? ( <BreadcrumbLink render={item.render}> {item.label} </BreadcrumbLink> ) : item.to ? ( <BreadcrumbLink render={ <Link to={item.to} params={item.params} search={item.search} /> } onClick={(e) => e.stopPropagation()} > {item.label} </BreadcrumbLink> ) : ( <BreadcrumbPage>{item.label}</BreadcrumbPage> )} </BreadcrumbItem> {!isLastItem ? <BreadcrumbSeparator /> : null} </Fragment> ); })} </BreadcrumbList> </Breadcrumb> );}export { BreadCrumb, type BreadCrumbItem };Usage
import { BreadCrumb } from "@/components/recipes/breadcrumb";
<BreadCrumb items={[{ label: "Dashboard" }, { label: "Applications" }]} />Examples
Default Breadcrumb
Use this for product pages that need responsive crumb collapsing.
Props
| Prop/API | Type | Default | Description |
|---|---|---|---|
items | BreadCrumbItem[] | - | Breadcrumb labels and optional route targets. |
maxVisibleMobile | number | 2 | Number of visible items on mobile. |
maxVisibleDesktop | number | 4 | Number of visible items on larger screens. |
itemsBeforeCollapse | number | 1 | Leading items kept before collapsed middle items. |
Accessibility
- Keep the final item as the current page.
- Provide meaningful labels, not route ids.
- Ensure collapsed menu items remain keyboard reachable.