Skip to content

Breadcrumb

A navigational landmark that shows the user's current location in a hierarchy. Renders a semantic <nav> with an ordered list of bit-breadcrumb-item links, separated by a customizable separator glyph.

Features

  • 📍 Semantic HTML: renders <nav><ol><li> for proper landmark semantics
  • active prop: marks the current page — adds aria-current="page" and disables the link
  • 🔗 Flexible hrefs: each item accepts an href for standard navigation
  • 🎨 Custom separator: override the separator character per-instance or via CSS variable
  • 🖼️ Icon slot: each item supports a leading icon slot
  • ARIA: aria-label on <nav>, aria-current="page" on active item, aria-disabled on the active link

Source Code

View Source Code
ts
import { define, prop, effect, html } from '@vielzeug/craftit';

import itemStyles from './breadcrumb-item.css?inline';

export type BitBreadcrumbProps = {
  label?: string;
  separator?: string;
};

export type BitBreadcrumbItemProps = {
  active?: boolean;
  href?: string;
  separator?: string;
};

/**
 * A single breadcrumb entry rendered inside `<bit-breadcrumb>`.
 *
 * @element bit-breadcrumb-item
 *
 * @attr {boolean} active - Marks this item as the current page (`aria-current="page"`)
 * @attr {string} href - Link target for this breadcrumb item
 * @attr {string} separator - Separator text shown before the item (except first item)
 *
 * @slot - Item label/content
 * @slot icon - Optional leading icon for the crumb
 *
 * @part separator - Separator element before the item label
 * @part link - The interactive anchor element
 *
 * @example
 * ```html
 * <bit-breadcrumb-item href="/">Home</bit-breadcrumb-item>
 * <bit-breadcrumb-item active>Current Page</bit-breadcrumb-item>
 * ```
 */
export const BREADCRUMB_ITEM_TAG = define<BitBreadcrumbItemProps>('bit-breadcrumb-item', {
  props: {
    active: prop.bool(),
    href: prop.string(''),
    separator: prop.string('/'),
  },
  setup(props) {
    return () => html`
      <li class="item" role="listitem">
        <span class="separator" part="separator" aria-hidden="true">${props.separator}</span>
        <a
          class="link"
          :href="${props.href}"
          :aria-current="${() => (props.active.value ? 'page' : null)}"
          :tabindex="${() => (props.active.value ? '-1' : null)}"
          part="link">
          <span class="icon"><slot name="icon"></slot></span>
          <span class="label"><slot></slot></span>
        </a>
      </li>
    `;
  },
  styles: [itemStyles],
});

// ============================================
// Breadcrumb Component
// ============================================

import componentStyles from './breadcrumb.css?inline';

/**
 * Accessible breadcrumb navigation container.
 *
 * @element bit-breadcrumb
 *
 * @attr {string} label - Accessible label for the internal `<nav>` landmark
 * @attr {string} separator - Separator text propagated to child `<bit-breadcrumb-item>` elements
 *
 * @slot - One or more `<bit-breadcrumb-item>` children
 *
 * @part nav - Internal navigation landmark element
 * @part list - Internal ordered list container
 *
 * @cssprop --breadcrumb-separator - Optional custom separator text synced to child items
 *
 * @example
 * ```html
 * <bit-breadcrumb label="Page breadcrumb">
 *   <bit-breadcrumb-item href="/">Home</bit-breadcrumb-item>
 *   <bit-breadcrumb-item href="/blog">Blog</bit-breadcrumb-item>
 *   <bit-breadcrumb-item active>My Post</bit-breadcrumb-item>
 * </bit-breadcrumb>
 * ```
 */
export const BREADCRUMB_TAG = define<BitBreadcrumbProps>('bit-breadcrumb', {
  props: {
    label: prop.string('Breadcrumb'),
    separator: prop.string(''),
  },
  setup(props, { host, slots }) {
    // ────────────────────────────────────────────────────────────────
    // Item & Separator Synchronization
    // ────────────────────────────────────────────────────────────────

    const getItems = (): HTMLElement[] => Array.from(host.el.getElementsByTagName('bit-breadcrumb-item'));

    const syncItems = () => {
      const sep = props.separator.value || '/';
      const items = getItems();

      for (let i = 0; i < items.length; i++) {
        items[i].setAttribute('separator', sep);
        items[i].toggleAttribute('data-show-separator', i > 0);
      }

      // Sync CSS variable for CSS-based separator (if used)
      if (sep) host.el.style.setProperty('--breadcrumb-separator', `'${sep}'`);
      else host.el.style.removeProperty('--breadcrumb-separator');
    };

    // ────────────────────────────────────────────────────────────────
    // Lifecycle
    // ────────────────────────────────────────────────────────────────

    effect(() => {
      void slots.elements().value;
      syncItems();
    });

    return () => html`
      <nav part="nav" :aria-label="${props.label}">
        <ol role="list" part="list">
          <slot></slot>
        </ol>
      </nav>
    `;
  },
  styles: [componentStyles],
});

Basic Usage

Wrap bit-breadcrumb-item elements inside bit-breadcrumb. Mark the current page with active.

html
<bit-breadcrumb>
  <bit-breadcrumb-item href="/">Home</bit-breadcrumb-item>
  <bit-breadcrumb-item href="/products">Products</bit-breadcrumb-item>
  <bit-breadcrumb-item active>Sneakers</bit-breadcrumb-item>
</bit-breadcrumb>

<script type="module">
  import '@vielzeug/buildit/breadcrumb';
</script>

Items with Icons

Use the icon named slot on any bit-breadcrumb-item for a leading icon.

PreviewCode
RTL

Custom Separator

Use the separator attribute to replace the default / separator. You can also override the --breadcrumb-separator CSS custom property globally in your theme.

PreviewCode
RTL

Custom aria-label

Override the default "Breadcrumb" landmark label when a page has multiple navigation regions.

PreviewCode
RTL

API Reference

bit-breadcrumb Attributes

AttributeTypeDefaultDescription
labelstring'Breadcrumb'aria-label applied to the wrapping <nav> landmark
separatorstring'/'Separator glyph rendered between items (also sets --breadcrumb-separator)

bit-breadcrumb Slots

SlotDescription
(default)bit-breadcrumb-item elements representing each crumb

bit-breadcrumb CSS Custom Properties

PropertyDescriptionDefault
--breadcrumb-separatorSeparator character shown between items'/'

bit-breadcrumb-item Attributes

AttributeTypeDefaultDescription
hrefstringURL the item links to. Omit for non-linked crumbs.
activebooleanfalseMarks this item as the current page (aria-current="page"). Disables the link.

bit-breadcrumb-item Slots

SlotDescription
iconOptional leading icon or decoration
(default)Crumb label text

Events

bit-breadcrumb and bit-breadcrumb-item do not emit custom events. Use standard DOM click listeners on individual items if you need to intercept navigation (e.g., in an SPA).

Accessibility

The breadcrumb component follows WAI-ARIA best practices.

bit-breadcrumb

Semantic Structure

  • Renders a <nav> element with aria-label matching the label attribute — it serves as a navigation landmark.
  • Items are rendered as <li> elements inside an <ol>, conveying the sequential structure to screen readers.

Screen Readers

  • The active item receives aria-current="page" and aria-disabled="true" so it is announced as the current location and not activated when clicked.
  • The separator is rendered via CSS content (or a hidden aria-hidden element) so it is not read aloud.
  • When using icon-only crumbs, provide visible text as the default slot content or add aria-label to the icon wrapper to keep the crumb meaningful to screen readers.

Best Practices

Do:

  • Always mark the final (current) crumb as active — omitting it breaks aria-current and confuses screen readers.
  • Keep crumb labels short and descriptive — they should match the <title> or <h1> of the destination page.
  • Provide an href on every non-active crumb so keyboard and screen reader users can navigate directly.

Don't:

  • Mark more than one crumb as active — only the current page should carry aria-current="page".
  • Use breadcrumbs as a replacement for primary navigation — they supplement, not replace, the main nav.
  • Omit the breadcrumb on mobile viewports — breadcrumbs are even more valuable on small screens where back-navigation is harder.