An ordered trail of links ending in the current page, for showing where the current page sits in a hierarchy.
Like TabNav, it does not use roving focus: every crumb is a link, so each one stays individually reachable by Tab and the arrow keys are left to the browser.
import { Breadcrumbs } from 'frontile';
The docs site has no routes for Breadcrumbs to link to, so every demo on
this page uses @href rather than @route. In an app with routes, prefer
@route (below). It derives the current crumb from the router for you.
A crumb with no link target at all is the current page: <b.Item> needs no
arguments to say "you are here."
import { Breadcrumbs } from 'frontile';
<template>
<Breadcrumbs as |b|>
<b.Item @href='/'>Home</b.Item>
<b.Item @href='/library'>Library</b.Item>
<b.Item>Data</b.Item>
</Breadcrumbs>
</template>
Breadcrumbs has two authoring forms. The block form above gives full
control over each crumb; use it by default. The @items form, covered under
Collapsing long trails, renders the trail from a
plain array instead, which is worth it once the trail is long enough to
collapse automatically.
Pass @route (with @model, @models, or @query as needed) and b.Item
renders an Ember LinkTo, deriving its current state from the router. No
@isCurrent needed. The demo below isn't rendered, since the docs site has
no matching routes; it is here as reference.
import { Breadcrumbs } from 'frontile';
<template>
<Breadcrumbs as |b|>
<b.Item @route='library.index'>Library</b.Item>
<b.Item @route='library.item' @model={{@item.id}}>{{@item.title}}</b.Item>
</Breadcrumbs>
</template>
@isCurrent always wins over the router, in both directions. Pass it to
override a route crumb the router would otherwise mark current, or to force
one current that isn't.
Once a trail gets long, the middle can be collapsed behind an ellipsis marker. There are two ways to get one, matching the two authoring forms.
@items and @maxItemsPassing @items renders the trail from an array instead of from blocks, and
is what makes @maxItems meaningful. Yielded blocks can't be counted before
they render, so in the block form there's nothing for @maxItems to divide.
Once @items.length exceeds @maxItems, the middle collapses into an
ellipsis marker that announces how many crumbs it stands in for. It is the
same marker b.Ellipsis renders when you place one by hand, below.
import { Breadcrumbs } from 'frontile';
const trail = [
{ label: 'Home', href: '/' },
{ label: 'Library', href: '/library' },
{ label: 'Data', href: '/library/data' },
{ label: 'Reports', href: '/library/data/reports' },
{ label: 'Q3' }
];
<template>
<Breadcrumbs @items={{trail}} @maxItems={{3}} />
</template>
@itemsBeforeCollapse and @itemsAfterCollapse (each defaulting to 1)
move where the split happens:
import { Breadcrumbs } from 'frontile';
const trail = [
{ label: 'Home', href: '/' },
{ label: 'Library', href: '/library' },
{ label: 'Data', href: '/library/data' },
{ label: 'Reports', href: '/library/data/reports' },
{ label: 'Q3' }
];
<template>
<Breadcrumbs
@items={{trail}}
@maxItems={{4}}
@itemsBeforeCollapse={{2}}
@itemsAfterCollapse={{1}}
/>
</template>
Each entry in @items takes label (its text) plus the same arguments as
b.Item: route/model/models/query or href, isCurrent,
isDisabled. A crumb with neither route nor href is the current page,
exactly as in the block form.
<b.Ellipsis />In the block form, you place the ellipsis yourself. That helps when the trail doesn't come from a flat array, or the collapse point isn't a simple count:
import { Breadcrumbs } from 'frontile';
<template>
<Breadcrumbs as |b|>
<b.Item @href='/'>Home</b.Item>
<b.Ellipsis @hiddenCount={{2}} />
<b.Item>Q3</b.Item>
</Breadcrumbs>
</template>
@hiddenCount drives the visually-hidden announcement ("2 more levels"); pass
it whenever you know how many crumbs the marker stands in for. Omit it and
the announcement falls back to an uncounted "More levels" rather than making
you count your own crumbs.
Passing a block replaces the glyph and takes over the announcement entirely.
This is where a Dropdown listing the hidden crumbs goes. The block
yields hiddenCount and hiddenItems (empty unless you pass @hiddenItems),
so you can render the crumbs it stands in for:
import { Breadcrumbs } from 'frontile';
const hidden = [{ label: 'Library' }, { label: 'Data' }];
<template>
<Breadcrumbs as |b|>
<b.Item @href='/'>Home</b.Item>
<b.Ellipsis @hiddenItems={{hidden}} as |e|>
<button type='button'>
{{e.hiddenItems.length}}
hidden …
</button>
</b.Ellipsis>
<b.Item>Q3</b.Item>
</Breadcrumbs>
</template>
In the @items form, the same two blocks are available as named blocks:
:item to render every crumb yourself, and :ellipsis to render the
auto-placed marker. @maxItems still computes the split while you control
the markup:
import { Breadcrumbs } from 'frontile';
<template>
<Breadcrumbs @items={{@trail}} @maxItems={{3}}>
<:item as |ctx|>
<li class={{ctx.itemClass}}>
<span class={{ctx.linkClass}}>{{ctx.item.label}}</span>
</li>
</:item>
<:ellipsis as |e|>
<button type='button'>{{e.hiddenCount}} hidden</button>
</:ellipsis>
</Breadcrumbs>
</template>
@separator replaces the chevron glyph between crumbs with any component:
import { Breadcrumbs } from 'frontile';
const Slash = <template><span>/</span></template>;
<template>
<Breadcrumbs @separator={{Slash}} as |b|>
<b.Item @href='/'>Home</b.Item>
<b.Item @href='/library'>Library</b.Item>
<b.Item>Data</b.Item>
</Breadcrumbs>
</template>
@size (sm / md / lg, default md) scales the text and separator
glyph. @underline (always / hover / none, default hover) controls when
a crumb's link is underlined. The current crumb is never underlined in any
mode, since it doesn't go anywhere.
@color picks the hover and current-page ink, and takes neutral (the
default), primary or danger. That is three categories where most themed
components offer seven. The other four are fill colours, designed to carry
text-on-* text on top of them the way Pagination's active chip does. A
breadcrumb has no fill, so its colour lands on the text itself, and as ink on
a light surface those categories fall well below the contrast a reader needs:
success reaches only 2.3
import { Breadcrumbs } from 'frontile';
<template>
<div class='demo-stack items-start'>
<Breadcrumbs @size='sm' @color='primary' as |b|>
<b.Item @href='/'>Home</b.Item>
<b.Item>Small, primary</b.Item>
</Breadcrumbs>
<Breadcrumbs @size='lg' @color='danger' @underline='always' as |b|>
<b.Item @href='/'>Home</b.Item>
<b.Item>Large, danger, always underlined</b.Item>
</Breadcrumbs>
</div>
</template>
@isDisabled drops the href as well as marking the crumb aria-disabled.
An anchor can't be natively disabled, so removing the href is what actually
stops navigation. It only affects a linked crumb; an unlinked crumb (no
@route or @href) never receives aria-disabled, since it isn't a link to
begin with.
import { Breadcrumbs } from 'frontile';
<template>
<Breadcrumbs as |b|>
<b.Item @href='/'>Home</b.Item>
<b.Item @href='/library' @isDisabled={{true}}>Library</b.Item>
<b.Item>Data</b.Item>
</Breadcrumbs>
</template>
b also yields itemClass, linkClass, separatorClass, and setupItem
directly, for a link component other than b.Item: ember-link, or a custom
<AppLink>. Apply linkClass to the link's class, and {{b.setupItem isCurrent}} to its element, passing a boolean for whether it's the current
crumb. It then gets the same theme classes and ARIA as b.Item. Wrap it in an
<li> with itemClass, and add the separator yourself with separatorClass:
import { Breadcrumbs } from 'frontile';
<template>
<Breadcrumbs as |b|>
<li class={{b.itemClass}}>
<a href='/library' class={{b.linkClass}} {{b.setupItem false}}>
Library
</a>
<span class={{b.separatorClass}} aria-hidden='true'>/</span>
</li>
<li class={{b.itemClass}}>
<span class={{b.linkClass}} {{b.setupItem true}}>Data</span>
</li>
</Breadcrumbs>
</template>
Breadcrumbs renders a <nav> landmark with an accessible name from
@label (default 'Breadcrumb'), wrapping an <ol>, since the trail is an
ordered list. Every crumb stays in the natural tab order; there is no roving
tabindex and no keyboard handling beyond ordinary link navigation.
The current crumb carries aria-current="page" and is rendered as a
<span>, not a link, since it doesn't go anywhere. Breadcrumbs.Item
derives current from, in order: an explicit @isCurrent; the router, for a
@route crumb; then the fallback that a crumb with no link target at all is
the page you're on. Only the last statically-current crumb keeps
aria-current, since two would be invalid. Breadcrumbs warns if @items
produces more than one.
A separator follows every crumb, including the last, where it is hidden by
CSS rather than omitted. It carries aria-hidden="true" either way, so it
never reaches assistive technology.
An ellipsis marker with no block carries aria-hidden="true" on its glyph
and a visually-hidden announcement ("N more levels", or "More levels" without
a count). Supplying a block to b.Ellipsis suppresses that built-in
announcement, since the block's own content (typically a button that opens a
menu) carries its own accessible name.
Element: HTMLElement
An ordered trail of links ending in the current page.
Like TabNav, it deliberately does not use roving focus: these are
links, so every one of them stays individually reachable by Tab and the
arrow keys are left to the browser.
| Name | Type | Default | Description |
|---|---|---|---|
classes
|
SlotsToClasses<'base' | 'item' | 'separator' | 'list' | 'link' | 'ellipsis'>
|
- | Class names for each slot of the component, merged with the theme's. |
color
|
enum
|
'neutral'
|
The colour of the hover and current-page ink. Three categories rather than the usual seven: the others are fill colours,
meant to carry |
items
|
Array
|
- |
Renders the trail from an array instead of from blocks. Passing it is what
makes @maxItems meaningful -- yielded blocks cannot be counted before
they render, so in the block form there is nothing for it to divide.
|
itemsAfterCollapse
|
number
|
1
|
|
itemsBeforeCollapse
|
number
|
1
|
|
label
|
string
|
'Breadcrumb'
|
Accessible name for the navigation landmark. |
maxItems
|
number
|
- |
Collapses the middle of the trail once there are more crumbs than this.
@items form only.
|
separator
|
ComponentLike<{ Element: SVGElement; }>
|
ChevronRightIcon
|
Replaces the separator glyph. A component argument rather than a named
block because the separator is rendered inside each <li>, which Item
owns -- a named block on this component cannot be handed down to a child.
|
size
|
enum
|
'md'
|
|
underline
|
enum
|
'hover'
|
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- | |
item
*
|
Array
|
- | |
ellipsis
*
|
Array
|
- |
Element: HTMLElement
One crumb: its <li>, the crumb itself, and the separator that follows it.
The separator is rendered here rather than by the root, and hidden on the last crumb by CSS. That is what lets both authoring forms work without either one knowing an item's position.
| Name | Type | Default | Description |
|---|---|---|---|
itemClass
*
|
string
|
- | Supplied by Breadcrumbs. Not part of the public API. |
linkClass
*
|
string
|
- | |
separatorClass
*
|
string
|
- | |
setupItem
*
|
ModifierLike<{ Element: HTMLElement; Args: { Positional: [boolean]; }; }>
|
- | |
class
|
string
|
- | Class names appended to this crumb's theme classes. |
href
|
string
|
- | Renders a plain anchor. |
isCurrent
|
boolean
|
- | Overrides the current-page state. Wins over every other rule. |
isDisabled
|
boolean
|
false
|
Marks the crumb as disabled. An anchor cannot be natively disabled, so the
href is dropped as well -- aria-disabled alone still leaves it
clickable. Only affects a linked crumb: an unlinked crumb (no @route or
@href) renders as a <span> and never receives aria-disabled or
data-disabled regardless of this arg.
|
model
|
unknown
|
- |
A single dynamic segment for @route.
|
models
|
Array
|
- |
Dynamic segments for @route.
|
query
|
Record<string, unknown>
|
- |
Query params for @route.
|
route
|
string
|
- |
Renders a LinkTo for this route and derives the current state from the
router. Omit it (and pass @href) to stay entirely router-free.
|
separator
|
ComponentLike<{ Element: SVGElement; }>
|
- |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |
Element: HTMLElement
The gap marker standing in for a run of crumbs.
It renders its own <li> including a separator, structurally identical to
what Item renders -- a bare <span> would fall outside the
group-last/item:hidden scheme that hides the trailing separator, and the
trail would end in a dangling chevron whenever an ellipsis came last.
| Name | Type | Default | Description |
|---|---|---|---|
ellipsisClass
*
|
string
|
- | |
itemClass
*
|
string
|
- | |
separatorClass
*
|
string
|
- | |
class
|
string
|
- | Class names appended to this marker's theme classes. |
hiddenCount
|
number
|
- |
Drives the visually-hidden announcement. The @items form passes it; in
the block form an author may supply it, and without it the announcement
falls back to an uncounted one rather than making the author count their
own crumbs.
|
hiddenItems
|
Array
|
- | The crumbs this marker stands in for. Yielded straight back to the block. |
separator
|
ComponentLike<{ Element: SVGElement; }>
|
- |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |
Replaces the glyph. This is where a Dropdown goes.
|