Gecko UIGecko UI

Breadcrumb

The trail of pages above the one you are on

Breadcrumb

The trail of pages above the one you are on.

Installation

import { Breadcrumb, BreadcrumbItem } from '@geckoui/geckoui';

Basic Usage

Give each crumb your own router link with asChild. This is how a breadcrumb is meant to be used. Client routing is what you want in almost every app, and asChild is what keeps it.

routeclick a crumb
import Link from 'next/link';
import { Breadcrumb, BreadcrumbItem } from '@geckoui/geckoui';

<Breadcrumb>
  <BreadcrumbItem asChild>
    <Link href="/">Home</Link>
  </BreadcrumbItem>
  <BreadcrumbItem asChild>
    <Link href="/settings">Settings</Link>
  </BreadcrumbItem>
  <BreadcrumbItem>Profile</BreadcrumbItem>
</Breadcrumb>

asChild hands the crumb to your element rather than rendering an anchor of its own, so the link keeps its own behaviour: prefetching, no reload, and whatever else your router does. The child keeps its class name, with the crumb's added alongside.

The last crumb is the page you are on, so it is drawn as text rather than a link and carries aria-current="page".

Routing without a reload

route/library/data
import Link from 'next/link';
import { Breadcrumb, BreadcrumbItem } from '@geckoui/geckoui';

const TRAIL = [
  ['/', 'Home'],
  ['/library', 'Library'],
  ['/library/data', 'Data']
];

export function Trail() {
  return (
    <Breadcrumb>
      {TRAIL.map(([href, label]) => (
        <BreadcrumbItem key={label} asChild>
          <Link href={href}>{label}</Link>
        </BreadcrumbItem>
      ))}
    </Breadcrumb>
  );
}

Works the same with any router — React Router's Link, TanStack Router's Link, or your own.

A plain anchor

href is there for when you have no router: a static site, a multi page app, or a link off to somewhere else.

<Breadcrumb>
  <BreadcrumbItem href="/">Home</BreadcrumbItem>
  <BreadcrumbItem href="/settings">Settings</BreadcrumbItem>
  <BreadcrumbItem>Profile</BreadcrumbItem>
</Breadcrumb>

It renders a plain <a href>, so the browser reloads the page. Inside a router app reach for asChild instead.

A crumb that runs code

When there is no link to hand, an onClick makes the crumb a <button>: it joins the tab order and answers Enter and Space.

routeclick a crumb
import { useRouter } from 'next/navigation';
import { Breadcrumb, BreadcrumbItem } from '@geckoui/geckoui';

export function Trail() {
  const router = useRouter();

  return (
    <Breadcrumb>
      <BreadcrumbItem onClick={() => router.push('/')}>Home</BreadcrumbItem>
      <BreadcrumbItem onClick={() => router.push('/library')}>Library</BreadcrumbItem>
      <BreadcrumbItem>Data</BreadcrumbItem>
    </Breadcrumb>
  );
}

A link is still better where you have one: it can be opened in a new tab, copied, and read as a link.

Props API

PropTypeDefaultDescription
separatorReactNodea chevronWhat goes between the crumbs
maxItemsnumber-How many to show before the middle is folded away
itemsBeforeCollapsenumber1How many to keep at the start when folded
itemsAfterCollapsenumber1How many to keep at the end when folded
expandLabelstring'Show the rest'What the ellipsis is called
menuPlacementPlacement'bottom-start'Where the folded list opens
menuClassNamestring-CSS class for that list
aria-labelstring'Breadcrumb'What the whole trail is called
sizekeyof BreadcrumbSizeMap'md'sm, md or lg
classNamestring-CSS class for the nav
PropTypeDefaultDescription
asChildbooleanfalseHand the crumb to your own element. The way to use a router link
hrefstring-Where it goes, as a plain <a>. Reloads the page, so it is for apps without a router
onClickMouseEventHandler-Makes the crumb a <button> when there is no href
currentbooleanlast crumbWhether this is the page you are on
...restAnchorHTMLAttributes-All standard anchor attributes

Examples

A long trail

routeclick a crumb
import Link from 'next/link';
import { Breadcrumb, BreadcrumbItem } from '@geckoui/geckoui';

const TRAIL = [
  ['/', 'Home'],
  ['/library', 'Library'],
  ['/library/data', 'Data'],
  ['/library/data/2026', '2026'],
  ['/library/data/2026/q3', 'Q3'],
  [undefined, 'September']
];

<Breadcrumb maxItems={3}>
  {TRAIL.map(([href, label]) => (
    <BreadcrumbItem key={label} asChild={Boolean(href)}>
      {href ? <Link href={href}>{label}</Link> : label}
    </BreadcrumbItem>
  ))}
</Breadcrumb>

// keep two at each end instead of one
<Breadcrumb maxItems={4} itemsBeforeCollapse={2} itemsAfterCollapse={2}>
  {TRAIL.map(([href, label]) => (
    <BreadcrumbItem key={label} asChild={Boolean(href)}>
      {href ? <Link href={href}>{label}</Link> : label}
    </BreadcrumbItem>
  ))}
</Breadcrumb>

Past maxItems the middle is folded away behind an ellipsis, which opens it as a list. The ends are what is kept, because they are what tell you where you started and where you are.

The folded crumbs open as a list rather than unfolding in place. On a narrow screen unfolding a deep trail only wraps it over three lines, which is no easier to read than the ellipsis was.

Separators

routeclick a crumb
<Breadcrumb separator="/">
  <BreadcrumbItem asChild>
    <Link href="/">Home</Link>
  </BreadcrumbItem>
  <BreadcrumbItem asChild>
    <Link href="/docs">Docs</Link>
  </BreadcrumbItem>
  <BreadcrumbItem>Breadcrumb</BreadcrumbItem>
</Breadcrumb>

<Breadcrumb separator={<span className="text-xs">•</span>}>
  <BreadcrumbItem asChild>
    <Link href="/">Home</Link>
  </BreadcrumbItem>
  <BreadcrumbItem asChild>
    <Link href="/docs">Docs</Link>
  </BreadcrumbItem>
  <BreadcrumbItem>Breadcrumb</BreadcrumbItem>
</Breadcrumb>

Icons

routeclick a crumb
<BreadcrumbItem asChild>
  <Link href="/">
    <HomeIcon /> Home
  </Link>
</BreadcrumbItem>

There is no icon prop. An icon is part of the crumb, like anything else in it.

When the last crumb is not where you are

routeclick a crumb
<Breadcrumb>
  <BreadcrumbItem asChild>
    <Link href="/">Home</Link>
  </BreadcrumbItem>
  <BreadcrumbItem current>Settings</BreadcrumbItem>
  <BreadcrumbItem asChild>
    <Link href="/settings/profile">Profile</Link>
  </BreadcrumbItem>
</Breadcrumb>

current moves the marker rather than adding a second one: the last crumb gives it up and goes back to being a link. There is only ever one page you are on.

Sizes

routeclick a crumb

Styling with CSS

VariableDefaultApplies to
--gecko-breadcrumb-link--color-text-primaryThe crumbs you can go to
--gecko-breadcrumb-current--color-text-tertiaryThe page you are on
--gecko-breadcrumb-separator--color-text-placeholderWhat goes between
--gecko-breadcrumb-gapper sizeSpace around each separator
--gecko-breadcrumb-separator-sizeper sizeThe chevron

The links are the loud ones and the current page is quiet, with the separators quieter again. You already know where you are; what the trail is for is the way back.

.GeckoUIBreadcrumb {
  --gecko-breadcrumb-link: var(--color-primary-600);
}

The list wraps rather than overflowing, because a trail running off the edge of the screen tells you nothing.

Class names and data attributes

  • GeckoUIBreadcrumb - The nav, with data-size
  • GeckoUIBreadcrumb__list - The ordered list
  • GeckoUIBreadcrumb__crumb - One item
  • GeckoUIBreadcrumb__link - The crumb itself, with data-current on the page you are on
  • GeckoUIBreadcrumb__separator - What goes between
  • GeckoUIBreadcrumb__expand - The ellipsis
  • GeckoUIBreadcrumb__menu - The folded crumbs, with __item for each

Accessibility

It renders a named <nav> around an <ol>, which is what a screen reader expects of a trail: the list gives the order and the depth without the separators being read out, so they are hidden.

The page you are on carries aria-current="page", and there is only ever one. The ellipsis says whether it is open.

A crumb with an onClick and no href is a real <button>, so it takes focus and answers the keyboard. It is never a <span> with a handler on it.

  • Tabs - For moving between pages at the same level
  • Pagination - For moving through a list
  • Menu - For actions rather than a trail