Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

LayoutReact

Breadcrumb

The trail of pages from the site's root to the one the reader is on: a navigation landmark named "Breadcrumb" around an ordered list, root first.

Rendered by React 19.3.1

import { Breadcrumb } from '@hamur/react/breadcrumb'

export default function BreadcrumbExample() {
  return (
    <Breadcrumb.Root>
      <Breadcrumb.List className="breadcrumb-list">
        <Breadcrumb.Item className="breadcrumb-item">
          <Breadcrumb.Link className="breadcrumb-link" href="#home">
            Home
          </Breadcrumb.Link>
        </Breadcrumb.Item>
        <Breadcrumb.Separator className="breadcrumb-separator" />
        <Breadcrumb.Item className="breadcrumb-item">
          <Breadcrumb.Link className="breadcrumb-link" href="#components">
            Components
          </Breadcrumb.Link>
        </Breadcrumb.Item>
        <Breadcrumb.Separator className="breadcrumb-separator" />
        <Breadcrumb.Item className="breadcrumb-item">
          <Breadcrumb.Link className="breadcrumb-link" href="#navigation">
            Navigation
          </Breadcrumb.Link>
        </Breadcrumb.Item>
        <Breadcrumb.Separator className="breadcrumb-separator" />
        <Breadcrumb.Item className="breadcrumb-item">
          <Breadcrumb.Page className="breadcrumb-page">
            Breadcrumb
          </Breadcrumb.Page>
        </Breadcrumb.Item>
      </Breadcrumb.List>
    </Breadcrumb.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Breadcrumb.Root>
  <Breadcrumb.List>
    <Breadcrumb.Item>
      <Breadcrumb.Link />
    </Breadcrumb.Item>
    <Breadcrumb.Separator>
      <Breadcrumb.Page />
    </Breadcrumb.Separator>
  </Breadcrumb.List>
</Breadcrumb.Root>

// Not in this example: Breadcrumb.Collection, Breadcrumb.Ellipsis

Examples

Collapsed

Six levels through Collection with maxItems 4 and itemsAfterCollapse 2: the three in the middle go behind the ellipsis, whose button opens a menu of them as links.

BreadcrumbCollapsed.tsx
import { Breadcrumb } from '@hamur/react/breadcrumb'
import { Menu } from '@hamur/react/menu'

interface Level {
  label: string
  href: string
}

const levels: Level[] = [
  { label: 'Home', href: '#home' },
  { label: 'Docs', href: '#docs' },
  { label: 'Components', href: '#components' },
  { label: 'Navigation', href: '#navigation' },
  { label: 'Breadcrumb', href: '#breadcrumb' },
  { label: 'API', href: '#api' },
]

export default function BreadcrumbCollapsedExample() {
  return (
    <Breadcrumb.Root>
      <Breadcrumb.List className="breadcrumb-list">
        <Breadcrumb.Collection
          items={levels}
          maxItems={4}
          itemsAfterCollapse={2}
        >
          {(entry) => (
            <>
              {(entry.type === 'ellipsis' ? entry.start === 0 : entry.first) ? null : (
                <Breadcrumb.Separator className="breadcrumb-separator" />
              )}
              <Breadcrumb.Item className="breadcrumb-item">
                {entry.type === 'ellipsis' ? (
                  <Menu.Root>
                    <Menu.Trigger className="breadcrumb-ellipsis-trigger" aria-label={entry.label}>
                      <Breadcrumb.Ellipsis />
                    </Menu.Trigger>
                    <Menu.Portal>
                      <Menu.Positioner sideOffset={6} align="start">
                        <Menu.Popup className="menu">
                          {entry.items.map((level) => (
                            <Menu.LinkItem
                              key={level.href}
                              className="menu-item breadcrumb-menu-link"
                              href={level.href}
                            >
                              {level.label}
                            </Menu.LinkItem>
                          ))}
                        </Menu.Popup>
                      </Menu.Positioner>
                    </Menu.Portal>
                  </Menu.Root>
                ) : entry.last ? (
                  <Breadcrumb.Page className="breadcrumb-page">
                    {entry.item.label}
                  </Breadcrumb.Page>
                ) : (
                  <Breadcrumb.Link
                    className="breadcrumb-link"
                    href={entry.item.href}
                  >
                    {entry.item.label}
                  </Breadcrumb.Link>
                )}
              </Breadcrumb.Item>
            </>
          )}
        </Breadcrumb.Collection>
      </Breadcrumb.List>
    </Breadcrumb.Root>
  )
}

Custom Separator

A chevron icon as the children of each Separator, flipped in a right-to-left page.

BreadcrumbCustomSeparator.tsx
import { Breadcrumb } from '@hamur/react/breadcrumb'

function ChevronIcon() {
  return (
    <svg className="breadcrumb-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
      <path d="M6 3.5 10.5 8 6 12.5" stroke="currentColor" strokeWidth="1.5" />
    </svg>
  )
}

export default function BreadcrumbCustomSeparatorExample() {
  return (
    <Breadcrumb.Root>
      <Breadcrumb.List className="breadcrumb-list">
        <Breadcrumb.Item className="breadcrumb-item">
          <Breadcrumb.Link className="breadcrumb-link" href="#home">
            Home
          </Breadcrumb.Link>
        </Breadcrumb.Item>
        <Breadcrumb.Separator className="breadcrumb-separator">
          <ChevronIcon />
        </Breadcrumb.Separator>
        <Breadcrumb.Item className="breadcrumb-item">
          <Breadcrumb.Link className="breadcrumb-link" href="#settings">
            Settings
          </Breadcrumb.Link>
        </Breadcrumb.Item>
        <Breadcrumb.Separator className="breadcrumb-separator">
          <ChevronIcon />
        </Breadcrumb.Separator>
        <Breadcrumb.Item className="breadcrumb-item">
          <Breadcrumb.Link className="breadcrumb-link" href="#account">
            Account
          </Breadcrumb.Link>
        </Breadcrumb.Item>
        <Breadcrumb.Separator className="breadcrumb-separator">
          <ChevronIcon />
        </Breadcrumb.Separator>
        <Breadcrumb.Item className="breadcrumb-item">
          <Breadcrumb.Page className="breadcrumb-page">
            Security
          </Breadcrumb.Page>
        </Breadcrumb.Item>
      </Breadcrumb.List>
    </Breadcrumb.Root>
  )
}

Router

Each Link renders through render as the app's link component, which stands in for a router: a click changes the route without loading a page, and the last level is an active Link.

BreadcrumbRouter.tsx
import * as React from 'react'
import { Breadcrumb } from '@hamur/react/breadcrumb'

const routes = [
  { label: 'Home', path: '/' },
  { label: 'Projects', path: '/projects' },
  { label: 'Base UI', path: '/projects/base-ui' },
  { label: 'Settings', path: '/projects/base-ui/settings' },
]

interface AppLinkProps extends React.ComponentPropsWithRef<'a'> {
  to: string
  onNavigate: (to: string) => void
}

function AppLink({ to, onNavigate, onClick, ...props }: AppLinkProps) {
  return (
    <a
      {...props}
      href={to}
      onClick={(event) => {
        onClick?.(event)
        if (event.defaultPrevented || event.button !== 0) return
        if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
        event.preventDefault()
        onNavigate(to)
      }}
    />
  )
}

export default function BreadcrumbRouterExample() {
  const [path, setPath] = React.useState(routes[routes.length - 1].path)
  const depth = routes.findIndex((route) => route.path === path)
  const trail = routes.slice(0, depth + 1)
  return (
    <div className="stack">
      <Breadcrumb.Root>
        <Breadcrumb.List className="breadcrumb-list">
          {trail.map((route, index) => (
            <React.Fragment key={route.path}>
              {index > 0 ? (
                <Breadcrumb.Separator className="breadcrumb-separator" />
              ) : null}
              <Breadcrumb.Item className="breadcrumb-item">
                <Breadcrumb.Link
                  className="breadcrumb-link"
                  active={index === trail.length - 1}
                  render={<AppLink to={route.path} onNavigate={setPath} />}
                >
                  {route.label}
                </Breadcrumb.Link>
              </Breadcrumb.Item>
            </React.Fragment>
          ))}
        </Breadcrumb.List>
      </Breadcrumb.Root>
      <p className="muted">Route {path}</p>
      <div>
        <button
          type="button"
          className="button"
          onClick={() => setPath(routes[routes.length - 1].path)}
        >
          Open Settings
        </button>
      </div>
    </div>
  )
}

API reference

7 props across 8 parts, in React syntax. className, style, render are listed once, under Shared props.

CollectionBreadcrumb.Collection

Calls its function child once per entry of the trail: each visible level, and one ellipsis entry for the levels a collapsed trail hides. Doesn’t render its own element.

Collection props
PropTypeDefault
itemsrequiredThe levels of the trail, root first.readonly Item[]—
maxItemsCollapses the trail when it has more levels than this.number—
itemsBeforeCollapseThe levels kept before the ellipsis.number1
itemsAfterCollapseThe levels kept after the ellipsis, at least 1, so the last level always shows.number1
childrenrequiredRenders one entry: { type: 'item', item, index, key, first, last } or(entry: BreadcrumbEntry<Item>, index: number) => React.ReactNode—

EllipsisBreadcrumb.Ellipsis

Stands for the levels a collapsed trail hides, … unless it has children. It is hidden from assistive technology: name the trigger around it with the Collection entry's label. Renders a <span>.

ItemBreadcrumb.Item

One level of the trail: holds a Breadcrumb.Link, a Breadcrumb.Page, or the trigger around a Breadcrumb.Ellipsis. Renders a <li>.

A link to a level of the trail. Use render for a router's link. Renders an <a>.

Link props
PropTypeDefault
activeWhether the link is the current page: it gets aria-current="page" and keeps its href.booleanfalse
Link data attributes
Data attributeDescription
data-activePresent when the link is the current page.

ListBreadcrumb.List

The ordered list of levels, from the root to the current page. Renders an <ol>.

PageBreadcrumb.Page

The current page as text, not a link. It is not focusable. Renders a <span>.

RootBreadcrumb.Root

Groups all parts of the breadcrumb: a navigation landmark of the trail of links to the current page, named "Breadcrumb" unless it is given aria-label or aria-labelledby. Renders a <nav>.

Root props
PropTypeDefault
labelsThe texts of the breadcrumb: root, the landmark's name when it has no aria-label orPartial<BreadcrumbLabels>—

SeparatorBreadcrumb.Separator

The mark between two levels, / unless it has children. It is hidden from assistive technology. Renders a <li>.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
TabMoves to the next link, in DOM order; the trail adds no keys of its ownLink
TabLeaves the trail after the last link: the current page as text is not a Tab stopPage
EnterOn the ellipsis button: opens the menu of the hidden levels, each a linkCollection

Accessibility

What the parts set
PartSetsSo that
Roota nav named "Breadcrumb" (labels, or your aria-label or aria-labelledby, with no default beside it)the trail is a landmark a screen reader lists and jumps to; name each one when a page has two
Listan ol with no role and no ARIAthe levels are read as an ordered list, root first
Linkwith active, aria-current="page" and data-active, keeping its href; an aria-current you give winsthe current page is read as "current page, link" and still opens in a new tab
Pagea span with aria-current="page", no role, no aria-disabled, no tabindexthe current page is not announced as a link no one can follow, and Tab skips it
Separatoran li with aria-hidden="true"the marks are not read between the levels nor counted as list items, and never join a link's name
Ellipsisaria-hidden="true", … unless it has childrenthe glyph is not read; the button around it carries the entry's label as its name
Collectionno element; the ellipsis entry's label with {count} replacedthe button for the hidden levels has a name in words

Focus order

  1. Each link is a Tab stop in DOM order, root first; the ellipsis button sits in its level's place. Nothing takes focus on mount.
  2. The current page as Page is not a Tab stop; as an active Link it is.
  3. The menu of hidden levels moves focus into its popup when opened with the keyboard, and back to the ellipsis button when closed.