Components

Getting started

Components154

More

Where you are and the way back: a long trail folds its middle into "…", which opens the hidden crumbs in place one after another.

Used forFile browsersDocsAdmin panelsShops

Install with the shadcn CLI

$ npx shadcn@latest add https://hairlineui.com/r/breadcrumb.json

Templates

Need the whole page?

43 Next.js landing pages, each designed from scratch. Try any of them live; one is $39, all of them $99, and 5 are free.

All 43 templates

Where it’s used

  • File browsers

    Workspace / Projects / Northwind / Assets.

  • Docs

    Section and page above the title.

  • Admin panels

    Customers / Ada Okafor / Invoices.

  • Shops

    Category trails above a product.

What it handles for you

Breadcrumbs fail on deep trails; folding keeps one line without losing the way back.

Instead of wiring upbreadcrumbtruncation logic
  1. Folding middle

    First and last two stay; the rest fold into "…".

  2. Opens in place

    Widths ease open with a 40ms stagger.

  3. Current page

    aria-current="page", not a link.

  4. Links or buttons

    href for routing, onClick for app state.

Overview

Long breadcrumbs either wrap onto two lines or cut the middle off for good. Here, past a limit, the middle folds into "…"; pressing it opens the hidden crumbs in place, each easing its width open with a short stagger. The last crumb is the current page, not a link.

States

folded
"…" stands in for the middle crumbs.
opened
Each hidden crumb's width eases open (380ms, 40ms apart) and fades in.

Usage

usage.tsx
import { Breadcrumb } from "@/components/ui/breadcrumb";<Breadcrumb items={[{ label: "Projects", href: "/projects" }, { label: "Northwind", href: "/projects/northwind" }, { label: "Assets" }]} />

Props

PropTypeDescription
itemsCrumb[]label, href, icon, onClick.
maxnumberdefault 4Crumbs before the middle folds.

Accessibility and motion

  • A nav landmark labelled "Breadcrumb" with an ordered list.
  • Reduced motion: the crumbs appear at once.

Requirements

React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

The CLI also installs ui-interaction-motion.

Source

components/breadcrumb.tsx
"use client";import * as React from "react";import { EASE_OUT, INTERACTION_FOCUS, useInteractionReducedMotion } from "./ui-interaction-motion";/* ───────────────────────────────────────────────────────── * BREADCRUMB: where you are, and the way back * *   long     past `max` crumbs (or on a narrow screen) the middle *            folds into "…"; pressing it opens them in place, each *            crumb's width easing open with a short stagger *   current  the last crumb is the page you're on: not a link, *            aria-current="page" * ───────────────────────────────────────────────────────── */export interface Crumb {  label: string;  href?: string;  icon?: React.ReactNode;  onClick?: () => void;}export interface BreadcrumbProps {  items: Crumb[];  /** Crumbs shown before the middle folds; the first and the last two stay. */  max?: number;  className?: string;}function Slash() {  return (    <svg viewBox="0 0 16 16" aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground/60">      <path d="M10 3.5 6 12.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />    </svg>  );}export function Breadcrumb({ items, max = 4, className = "" }: BreadcrumbProps) {  const reduced = useInteractionReducedMotion();  const [expanded, setExpanded] = React.useState(false);  const fold = items.length > max && !expanded;  // Folded crumbs: everything between the first and the last two.  const hiddenFrom = 1;  const hiddenTo = items.length - 2;  return (    <nav aria-label="Breadcrumb" className={className}>      <ol className="flex min-w-0 flex-wrap items-center gap-y-1 text-[13px]">        {items.map((item, i) => {          const last = i === items.length - 1;          const folds = items.length > max && i >= hiddenFrom && i < hiddenTo;          const crumbClass = `inline-flex items-center gap-1.5 whitespace-nowrap rounded-md [&_svg]:size-3.5 ${INTERACTION_FOCUS}`;          const body = (            <>              {item.icon}              {item.label}            </>          );          const node = last ? (            <span aria-current="page" className={`${crumbClass} font-medium text-foreground`}>              {body}            </span>          ) : item.href ? (            <a href={item.href} onClick={item.onClick} className={`${crumbClass} text-muted-foreground transition-colors duration-200 hover:text-foreground`}>              {body}            </a>          ) : (            <button type="button" onClick={item.onClick} className={`${crumbClass} text-muted-foreground cursor-pointer transition-colors duration-200 hover:text-foreground`}>              {body}            </button>          );          return (            <React.Fragment key={`${item.label}-${i}`}>              {folds && i === hiddenFrom && (                <li                  className="grid"                  style={{ gridTemplateColumns: fold ? "1fr" : "0fr", opacity: fold ? 1 : 0, transition: reduced ? "none" : `grid-template-columns 320ms ${EASE_OUT}, opacity 160ms ${EASE_OUT}` }}                  aria-hidden={!fold || undefined}                >                  <span className="flex min-w-0 items-center overflow-hidden">                    <button                      type="button"                      tabIndex={fold ? 0 : -1}                      aria-label={`Show ${hiddenTo - hiddenFrom} more`}                      onClick={() => setExpanded(true)}                      className={`mx-0.5 inline-flex h-6 items-center rounded-full px-1.5 text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground ${INTERACTION_FOCUS}`}                    >                      …                    </button>                    <Slash />                  </span>                </li>              )}              <li                className={folds ? "grid" : "flex items-center"}                aria-hidden={(folds && fold) || undefined}                style={                  folds                    ? {                        gridTemplateColumns: fold ? "0fr" : "1fr",                        opacity: fold ? 0 : 1,                        transition: reduced ? "none" : `grid-template-columns 380ms ${EASE_OUT} ${(i - hiddenFrom) * 40}ms, opacity 260ms ${EASE_OUT} ${(i - hiddenFrom) * 40 + 80}ms`,                      }                    : undefined                }              >                <span className={`flex min-w-0 items-center ${folds ? "overflow-hidden" : ""}`} inert={(folds && fold) || undefined}>                  {node}                  {!last && <Slash />}                </span>              </li>            </React.Fragment>          );        })}      </ol>    </nav>  );}export default Breadcrumb;

Registry item: /r/breadcrumb.json