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.
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
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.
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
Breadcrumb
| Prop | Type | Default | Description |
|---|---|---|---|
separator | ReactNode | a chevron | What goes between the crumbs |
maxItems | number | - | How many to show before the middle is folded away |
itemsBeforeCollapse | number | 1 | How many to keep at the start when folded |
itemsAfterCollapse | number | 1 | How many to keep at the end when folded |
expandLabel | string | 'Show the rest' | What the ellipsis is called |
menuPlacement | Placement | 'bottom-start' | Where the folded list opens |
menuClassName | string | - | CSS class for that list |
aria-label | string | 'Breadcrumb' | What the whole trail is called |
size | keyof BreadcrumbSizeMap | 'md' | sm, md or lg |
className | string | - | CSS class for the nav |
BreadcrumbItem
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Hand the crumb to your own element. The way to use a router link |
href | string | - | Where it goes, as a plain <a>. Reloads the page, so it is for apps without a router |
onClick | MouseEventHandler | - | Makes the crumb a <button> when there is no href |
current | boolean | last crumb | Whether this is the page you are on |
| ...rest | AnchorHTMLAttributes | - | All standard anchor attributes |
Examples
A long trail
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
<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
<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
<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
Styling with CSS
| Variable | Default | Applies to |
|---|---|---|
--gecko-breadcrumb-link | --color-text-primary | The crumbs you can go to |
--gecko-breadcrumb-current | --color-text-tertiary | The page you are on |
--gecko-breadcrumb-separator | --color-text-placeholder | What goes between |
--gecko-breadcrumb-gap | per size | Space around each separator |
--gecko-breadcrumb-separator-size | per size | The 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, withdata-sizeGeckoUIBreadcrumb__list- The ordered listGeckoUIBreadcrumb__crumb- One itemGeckoUIBreadcrumb__link- The crumb itself, withdata-currenton the page you are onGeckoUIBreadcrumb__separator- What goes betweenGeckoUIBreadcrumb__expand- The ellipsisGeckoUIBreadcrumb__menu- The folded crumbs, with__itemfor 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.
Related Components
- Tabs - For moving between pages at the same level
- Pagination - For moving through a list
- Menu - For actions rather than a trail