Breadcrumb
The trail back up from where you are.
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/lib/components/ui/breadcrumb";
export default function BreadcrumbDemo() {
return Breadcrumb(
BreadcrumbList(
BreadcrumbItem(BreadcrumbLink({ href: "/docs" }, "Docs")),
BreadcrumbSeparator(),
BreadcrumbItem(BreadcrumbEllipsis()),
BreadcrumbSeparator(),
BreadcrumbItem(BreadcrumbLink({ href: "/ui" }, "UI")),
BreadcrumbSeparator(),
BreadcrumbItem(BreadcrumbPage("Breadcrumb")),
),
);
}Installation
npx jsrepo add @implementjs/ui/breadcrumb
It installs @implementjs/lucide at the same time.
Copy the file below to src/lib/components/ui/breadcrumb.ts. It imports cn, so copy utils.ts to src/lib/utils.ts too. Then, on top of @implementjs/core and @implementjs/primitives:
npm install @implementjs/lucide
import { A, Li, Nav, Ol, Span, type Child, type ElementProps } from "@implementjs/core";
import { ChevronRightIcon, EllipsisIcon } from "@implementjs/lucide";
import { createComponent } from "@implementjs/primitives";
import { cn } from "@/lib/utils";
export type BreadcrumbProps = ElementProps<"nav">;
export type BreadcrumbListProps = ElementProps<"ol">;
export type BreadcrumbItemProps = ElementProps<"li">;
export type BreadcrumbLinkProps = ElementProps<"a">;
export type BreadcrumbPageProps = ElementProps<"span">;
export type BreadcrumbSeparatorProps = ElementProps<"li">;
export type BreadcrumbEllipsisProps = ElementProps<"span">;
/**
* The trail back up from where you are. The root is a labelled `nav`, the
* trail an ordered list, and the last crumb is a `BreadcrumbPage` rather
* than a link — you do not link to the page you are on.
*/
export const Breadcrumb = createComponent(function Breadcrumb(
{ class: className, ...props }: BreadcrumbProps,
...children: Child[]
) {
return Nav(
{ "aria-label": "breadcrumb", ...props, "data-slot": "breadcrumb", class: className },
...children,
);
});
export const BreadcrumbList = createComponent(function BreadcrumbList(
{ class: className, ...props }: BreadcrumbListProps,
...children: Child[]
) {
return Ol(
{
...props,
"data-slot": "breadcrumb-list",
class: cn(
"flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
className,
),
},
...children,
);
});
export const BreadcrumbItem = createComponent(function BreadcrumbItem(
{ class: className, ...props }: BreadcrumbItemProps,
...children: Child[]
) {
return Li(
{
...props,
"data-slot": "breadcrumb-item",
class: cn("inline-flex items-center gap-1.5", className),
},
...children,
);
});
export const BreadcrumbLink = createComponent(function BreadcrumbLink(
{ class: className, ...props }: BreadcrumbLinkProps,
...children: Child[]
) {
return A(
{
...props,
"data-slot": "breadcrumb-link",
class: cn("transition-colors hover:text-foreground", className),
},
...children,
);
});
/** The crumb for the current page: not a link, and marked as current. */
export const BreadcrumbPage = createComponent(function BreadcrumbPage(
{ class: className, ...props }: BreadcrumbPageProps,
...children: Child[]
) {
return Span(
{
role: "link",
"aria-disabled": true,
"aria-current": "page",
...props,
"data-slot": "breadcrumb-page",
class: cn("font-normal text-foreground", className),
},
...children,
);
});
/**
* The mark between crumbs. It is `aria-hidden` and presentational — the list
* structure already says these are steps, so announcing a chevron each time
* would only be noise. Pass children to use something other than a chevron.
*/
export const BreadcrumbSeparator = createComponent(function BreadcrumbSeparator(
{ class: className, ...props }: BreadcrumbSeparatorProps,
...children: Child[]
) {
return Li(
{
role: "presentation",
"aria-hidden": true,
...props,
"data-slot": "breadcrumb-separator",
class: cn("[&>svg]:size-3.5", className),
},
...(children.length > 0 ? children : [ChevronRightIcon()]),
);
});
/** A stand-in for crumbs that have been collapsed away. */
export const BreadcrumbEllipsis = createComponent(function BreadcrumbEllipsis(
{ class: className, ...props }: BreadcrumbEllipsisProps,
...children: Child[]
) {
return Span(
{
role: "presentation",
"aria-hidden": true,
...props,
"data-slot": "breadcrumb-ellipsis",
class: cn("flex size-9 items-center justify-center", className),
},
...children,
EllipsisIcon({ class: "size-4" }),
Span({ class: "sr-only" }, "More"),
);
});Usage
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/lib/components/ui/breadcrumb";
Breadcrumb(
BreadcrumbList(
BreadcrumbItem(BreadcrumbLink({ href: "/docs" }, "Docs")),
BreadcrumbSeparator(),
BreadcrumbItem(BreadcrumbPage("Breadcrumb")),
),
);
The last crumb
The page you are on is a BreadcrumbPage, not a BreadcrumbLink — you do not link to where you already are. It carries aria-current="page", which is what tells a screen reader the trail has ended.
Separators
BreadcrumbSeparator is aria-hidden and presentational. The ordered list already says these are steps, so announcing "chevron right" between each pair would only be noise. Pass children to use a different mark:
BreadcrumbSeparator(SlashIcon());
Collapsing a long trail
BreadcrumbEllipsis stands in for levels you have dropped. On a deep tree, wrap it in a dropdown menu so the hidden levels are still reachable.
API Reference
Breadcrumb
The trail. Labelled "breadcrumb" for screen readers. Renders a Nav; extra props are forwarded onto it.
BreadcrumbList
The ordered list of crumbs, wrapping on narrow screens. Renders a Ol; extra props are forwarded onto it.
BreadcrumbItem
One crumb. Renders a Li; extra props are forwarded onto it.
BreadcrumbLink
A crumb that navigates. Renders a A; extra props are forwarded onto it.
BreadcrumbPage
The crumb for the page you are on: not a link, and marked aria-current="page". Renders a Span; extra props are forwarded onto it.
BreadcrumbSeparator
The mark between crumbs — a chevron unless you pass your own. Presentational and aria-hidden: the list already says these are steps. Renders a Li; extra props are forwarded onto it.
BreadcrumbEllipsis
Stands in for crumbs that have been collapsed away. Renders a Span; extra props are forwarded onto it.