GrBreadcrumbs

  • core
  • navigation
  • API · 12 props
  • ⌨ keys

Shows where the user is and lets them step back up the path.

import { GrBreadcrumbs } from '@feugene/granularity/components/GrBreadcrumbs'

Overview

When to take it

  • the nesting is real — a catalogue, a file system, sections of documentation: the path shows where the user is;
  • the path is long — maxItems and autoCollapse fold the middle, leaving the beginning and the end;
  • every level can be returned to — each ancestor is a link, the last item is the current page;
  • the router is already connected — the links are drawn by GrLink, so the way of navigating is the same as everywhere.

When to take something else

NeedTake
Steps of a process rather than levelsGrTimeline
Sections of one levelGrTabs
The main navigation of the applicationGrSidebar / GrNavbar
A single step backGrButton / GrLink

A flat site needs no breadcrumbs: a path of one level says nothing and takes up space. They pay off where the user can end up on the fourth level and not remember the way.

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 3 each with a live demo and its code

  • Parents above the title in an app console

    The page names itself in the heading, so the path holds only its parents: current-index="-1" gives aria-current to nobody. The router owns the state — items carry to, as renders them with the app’s link component. Go down a few levels and the middle folds by max-items; the ellipsis named by expand-label opens it in place. A disabled organisation stays in the path as text, the house is iconOnly with its label kept for the screen reader, and «Environments» gets an ariaLabel that says whose.

    Variables
    DATABASE_URL = ••••••••STRIPE_KEY = ••••••••LOG_LEVEL = info
  • A deep path in a narrow panel

    Counting items predicts the width badly: three short folders fit, two long ones do not. auto-collapse measures — the path stays on one line and the middle folds exactly as much as the panel lacks, never below the last item. The ellipsis slot says how many folders it hides; the links lead to the real source tree.

  • The current group stays a link

    link-current keeps the last item clickable — here it opens the group anew, without the filter — and aria-current="page" stays on it. The item slot puts the size of the group next to its name, and separator="›" reads as a step down, not as part of a name.

API generated from the package · cannot drift

Props

PropTypedefaultDescription
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe type size of the path and its links — the scale is shared with `GrLink`. `md` by default; taken from `GrConfigProvider`.
ariaLabelstring | undefinedundefinedThe name of the landmark. If it is not set, it comes from the locale.
asGrBreadcrumbsLinkComponent | undefinedundefinedThe link component for every item: `RouterLink`, `NuxtLink`, Inertia’s `Link`.
separatorstring | undefinedundefinedThe separator between items. Decorative: it is not read to a screen reader.
maxItemsnumber | undefinedundefinedFrom how many items the middle folds into a "…". `0` or unset — show the whole path.
itemsBeforeCollapsenumber | undefined1How many items to leave at the start when folding.
itemsAfterCollapsenumber | undefined1How many items to leave at the end when folding.
linkCurrentboolean | undefinedfalseThe last item stays a link (the current page is clickable).
currentIndexnumber | undefinedundefinedWhich item is the current page. Unset — the last one. `-1` means the current page is not in the path: its name stands in the `h1`, and the path shows the ancestors only. Then nobody gets `aria-current`, and the last item stays an ordinary link.
expandLabelstring | undefinedundefinedThe i18n label of the button that expands a folded path.
autoCollapseboolean | undefinedfalseFold by the available width rather than by the number of items. The path becomes single-line, and the middle goes under the "…" by exactly as much as does not fit. Without the prop the behaviour is as before: the list wraps onto a second line, and the threshold is set by `maxItems`. Together the props are compatible — `maxItems` remains a hard ceiling, and the width squeezes further.
itemsrequiredGrBreadcrumbItem[]—The path from the root to the current page. The last item is the current page.

Slots

SlotTypeDescription
item{ item: GrBreadcrumbItem; index: number; isCurrent: boolean; }The content of an item as a whole (the icon and the label).
separatoranyThe separator between items.
ellipsis{ hiddenCount: number; expand: () => void; }The button that expands the folded middle.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrBreadcrumbsConfigurableProps, GrBreadcrumbsSize, GrBreadcrumbItem } from '@feugene/granularity/components/GrBreadcrumbs'

Filedist/types/components/GrBreadcrumbs/config.d.ts

grBreadcrumbsConfigconst
const grBreadcrumbsConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrBreadcrumbs">;

Filedist/types/components/GrBreadcrumbs/defaults.d.ts

GrBreadcrumbsConfigurablePropsinterface
interface GrBreadcrumbsConfigurableProps {
    size: GrBreadcrumbsSize;
    separator: string;
}

Filedist/types/components/GrBreadcrumbs/grBreadcrumbsStyles.d.ts

GrBreadcrumbsSizetype
type GrBreadcrumbsSize = GrComponentSize;
GrBreadcrumbItemtype
type GrBreadcrumbItem = {
    label: string;
    href?: string;
    to?: unknown;
    icon?: string | Component;
    iconOnly?: boolean;
    disabled?: boolean;
    ariaLabel?: string;
};
GrBreadcrumbsLayoutEntrytype
type GrBreadcrumbsLayoutEntry = {
    kind: 'item';
    item: GrBreadcrumbItem;
    index: number;
} | {
    kind: 'ellipsis';
    hiddenCount: number;
};
resolveBreadcrumbsFitfunction
function resolveBreadcrumbsFit(options: {
    itemWidths: number[];
    separatorWidth: number;
    ellipsisWidth: number;
    available: number;
    itemsBeforeCollapse: number;
    gapWidth?: number;
}): number;
resolveBreadcrumbsLayoutfunction
function resolveBreadcrumbsLayout(options: {
    items: GrBreadcrumbItem[];
    maxItems?: number;
    itemsBeforeCollapse: number;
    itemsAfterCollapse: number;
    expanded: boolean;
}): GrBreadcrumbsLayoutEntry[];

Filedist/types/components/GrBreadcrumbs/GrBreadcrumbs.vue.d.ts

GrBreadcrumbsPropsinterface
interface GrBreadcrumbsProps {
    items: GrBreadcrumbItem[];
    as?: GrBreadcrumbsLinkComponent;
    separator?: string;
    size?: GrBreadcrumbsSize;
    maxItems?: number;
    itemsBeforeCollapse?: number;
    itemsAfterCollapse?: number;
    ariaLabel?: string;
    linkCurrent?: boolean;
    currentIndex?: number;
    expandLabel?: string;
    autoCollapse?: boolean;
}

Filedist/types/components/GrBreadcrumbs/safelist.d.ts

grBreadcrumbsSafelistconst
const grBreadcrumbsSafelist: string[];

Accessibility

APG pattern

breadcrumb

Usage from the library prose

The router
vue
<GrBreadcrumbs :as="RouterLink" :items="items" />
ts
const items = [
  { label: 'Projects', to: '/projects' },
  { label: 'Granularity', to: '/projects/granularity' },
  { label: 'Settings' },
]

as sets the link component for every item at once, and to travels into it as a prop through the attrs of GrLink. Without as an item with href renders as an ordinary <a>.

The current page

The last item is not a link and is declared aria-current="page": it is that attribute that answers the screen reader’s question “where am I”. If the current page has to stay clickable (reloading a section, an anchor), switch on linkCurrent — aria-current is kept in that case.

An intermediate item can be switched off (disabled: true) — it becomes text, but does not get aria-current. It does not get the accent of the current page either: it is muted and of normal weight, like the neighbouring links — otherwise a highlighted ancestor at the start of the path would read as “you are here”. An intermediate item without href and to looks the same.

A long path
vue
<GrBreadcrumbs :items="path" :max-items="4" :items-before-collapse="1" :items-after-collapse="2" />

The middle folds into a ”…” button, and as many levels stay at the edges as itemsBeforeCollapse/itemsAfterCollapse ask for. The tail matters more than the head: “where am I now” is read from the right, so when there is not enough room it is the middle that is sacrificed.

The component will not hide a single item — the button would take up just as much room.

A click on the ellipsis expands the path in place, without a dropdown menu. The button disappears in the process, so the focus is moved to the first expanded item: otherwise it would end up on <body>. Changing items (a new page) folds the path again.

The layout can also be computed from the outside — resolveBreadcrumbsLayout is exported as a pure function.

Folding by the room available
vue
<GrBreadcrumbs :items="path" auto-collapse />

maxItems counts items, and on a narrow screen a number is a poor predictor: three short levels fit, two long ones do not. autoCollapse measures: the path becomes single-line, and the middle goes under the ”…” by exactly as much as does not fit.

What changes along with the mode:

without the propautoCollapse
a long pathwraps onto a second linestays on one line
the thresholdmaxItems (the number of items)the width of the container
the tailitemsAfterCollapseas much as fits, but not less than one

The head is not squeezed: itemsBeforeCollapse is usually the root item, and it is cheap. The tail is not squeezed below one item: the last one answers the question “where am I now”, and the path “Home / …” is useless.

When even “head, …, last” does not fit, the head, the separators and the ”…” keep their width, and the label of the last item is cut with an ellipsis. Squeezing everything equally is not an option: the link of the head is not truncated and would spill out of its item into the gap before the separator.

The props are compatible: maxItems remains a hard ceiling, and the width squeezes further.

The first frame in this mode is the full path: the widths of the labels are taken from it, and there is nowhere to get them before the render. After that the recalculation runs on ResizeObserver and costs one arithmetic operation — the labels do not depend on the width of the container.

The arithmetic is separated out: resolveBreadcrumbsFit is exported as a pure function if the decision has to be made from the outside.

The separator and the size

separator (/ by default) and size are read from GrConfigProvider:

vue
<GrConfigProvider :component-defaults="{ GrBreadcrumbs: { separator: '›' } }">

The separator is decorative — it lives in a list item of its own with aria-hidden: the structure of the path is told to the screen reader by the list itself, and it would read the ”/” aloud.

The icon of an item

The icon field is decorative (aria-hidden) and accepts a Vue component or an icon class (i-lucide-* — the application sets up the rule for it: in the engine factory or through a provider, otherwise a Vue component; see “Icons”).

iconOnly shows only the icon — the classic “house” at the start of the path:

vue
<GrBreadcrumbs :items="[{ label: 'Home', href: '/', icon: 'i-lucide-house', iconOnly: true }, …]" />

The label is not thrown away and does not move into aria-label; it is hidden with sr-only. The reason lies in who reads the path: a search engine and a screen reader read it as text, and an item without a name is empty for a blind user — they hear “link” without knowing where to.

Without icon the field is ignored, and in development the component warns: hiding the label without showing anything in its place means erasing the item — in the markup everything is in place, and the loss is visible to the eye alone.

Slots
SlotWhat it replaces
itemthe content of an item (item, index, isCurrent)
separatorthe separator
ellipsisthe expand button (hiddenCount, expand)