BreadCrumb

Responsive breadcrumb wrapper with collapsed overflow items.

hardening

Hardening

This component includes router integration. Prefer custom render links when integrating outside TanStack Router contexts.

Recipe Source

components/ui/breadcrumb.tsx
"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/APITypeDefaultDescription
itemsBreadCrumbItem[]-Breadcrumb labels and optional route targets.
maxVisibleMobilenumber2Number of visible items on mobile.
maxVisibleDesktopnumber4Number of visible items on larger screens.
itemsBeforeCollapsenumber1Leading 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.

On this page