GodUIGodUI
129Follow on X

Anatomy of Breadcrumbs

How a flat items array becomes a trail of pills with one shared hover highlight, a spring-loaded overflow popover, and a mobile collapse that never touches JavaScript.

01/04Pills and chevrons
Link crumb
navigable: text-muted-foreground
Current crumb
aria-current='page', filled bg-muted
Separator
aria-hidden chevron, one per gap
tsx
<motion.lilayoutinitial={reduceMotion ? false : { opacity: 0, x: -6 }}animate={{ opacity: 1, x: 0 }}exit={reduceMotion ? { opacity: 0 } : { opacity: 0, x: -6 }}transition={spring}>{isLast || !item.href ? (  <span aria-current={isLast ? "page" : undefined} className="... bg-muted text-foreground">    {item.label}  </span>) : (  <a href={item.href} className="... text-muted-foreground hover:text-foreground">    {item.label}  </a>)}</motion.li>
01

Pills and chevrons

A breadcrumb trail looks like the simplest nav in the system: links and a separator. The work is at the edges: what happens when the pointer moves along the row, and what happens when the trail is longer than the screen. Breadcrumbs handles both without adding an extra DOM node per crumb.

Every crumb but the last is an <a> tinted text-muted-foreground; the last is a plain <span> filled bg-muted and marked aria-current="page". An aria-hidden chevron sits between each pair. The whole row is a motion.ol so AnimatePresence can cross-fade crumbs in and out when the items array changes:

02/04One hover pill

layoutId spring: stiffness 520, damping 32

tsx
const HoverPill = ({ active }: { active: boolean }) =>active ? (  <motion.span    layoutId={hoverId}    transition={{ type: "spring", stiffness: 520, damping: 32 }}    className="absolute inset-0 rounded-lg bg-accent"  />) : null;
02

One hover pill

Every link shares a single motion.span bound by one layoutId; hovering a crumb does not toggle a background of its own. It only renders under whichever crumb is currently hovered, so moving the pointer along the trail springs that one element from box to box:

stiffness: 520, damping: 32 is the same snap used across GodUI's small UI springs. It is fast enough to keep up with a quick mouse sweep and damped enough that it doesn't wobble on arrival. onMouseEnter sets hovered to the crumb's key; leaving the <nav> entirely clears it in one onMouseLeave at the root instead of per-crumb listeners.

03/04Collapsing the middle

popover spring: scale 0.92 → 1, y -4 → 0

tsx
const tailCount = maxItems - 1;const head = items.slice(0, 1).map(toEntry);const hidden = items.slice(1, items.length - tailCount);const tail = items.slice(items.length - tailCount).map(toEntry);return [...head, { kind: "ellipsis", hidden }, ...tail];
03

Collapsing the middle

Pass maxItems, and once items.length exceeds it the middle crumbs fold into a single ellipsis button: head crumb, …, then the last maxItems − 1 crumbs. Clicking the ellipsis doesn't navigate; it opens a popover listing the crumbs it hides:

The popover springs in on scale: 0.92 → 1, y: -4 → 0, the same overlay entrance used by every other panel in the system, and a mousedown listener outside popRef closes it. Below maxItems, a second, CSS-only collapse kicks in on narrow viewports: crumbs past the first hide under max-sm:hidden and a static … takes their place. This collapse uses CSS instead of a useEffect viewport check because in the docs preview the component runs inside a portaled iframe but in the parent's JS realm, so only a media query sees the iframe's width.

04/04Result

Viewing /billing

maxItems={3}

04

The result

One items array, one shared hover pill, and a collapse that steps down from maxItems to a CSS ellipsis on narrow screens. The trail needs no markup beyond the pieces above.

Accessibility

The last crumb's aria-current="page" is the only signal for the current page, which is how a native breadcrumb should read to a screen reader. Separators are aria-hidden so they're invisible to assistive tech, and every link gets the standard focus-visible:ring-2. Crumbs are plain anchors, so they need no custom keyboard handling.

Motion Score

BreadcrumbsCC: Paint-triggering
SscaleScale spring / press
SopacityFade / cross-fade
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →