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 { defineComponent, effect, html, onMount, onSlotChange } from '@vielzeug/craftit';

// ============================================
// Types
// ============================================

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

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

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

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

/**
 * `bit-breadcrumb-item` — A single crumb within a `<bit-breadcrumb>` list.
 *
 * @example
 * ```html
 * <bit-breadcrumb-item href="/">Home</bit-breadcrumb-item>
 * <bit-breadcrumb-item active>Current Page</bit-breadcrumb-item>
 * ```
 */
export const BREADCRUMB_ITEM_TAG = defineComponent<BitBreadcrumbItemProps>({
  props: {
    active: { default: false },
    href: { default: '' },
    separator: { default: '/' },
  },
  setup({ props }) {
    return html`
      <li class="item" role="listitem">
        <span class="separator" part="separator" aria-hidden="true">${() => props.separator.value || '/'}</span>
        <a
          class="link"
          href="${() => props.href.value || undefined}"
          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],
  tag: 'bit-breadcrumb-item',
});

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

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

/**
 * `bit-breadcrumb` — Accessible navigation breadcrumb.
 *
 * Wrap `<bit-breadcrumb-item>` elements as children.
 * The last/current item should have `active` attribute.
 *
 * @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 = defineComponent<BitBreadcrumbProps>({
  props: {
    label: { default: 'Breadcrumb' },
    separator: { default: '' },
  },
  setup({ host, props }) {
    const getItems = (): HTMLElement[] => Array.from(host.getElementsByTagName('bit-breadcrumb-item')) as HTMLElement[];
    const syncSeparatorVar = () => {
      const sep = props.separator.value;

      if (sep) {
        host.style.setProperty('--breadcrumb-separator', `'${sep}'`);
      } else {
        host.style.removeProperty('--breadcrumb-separator');
      }
    };
    const syncItems = () => {
      const sep = props.separator.value || '/';
      const items = getItems();

      for (let i = 0; i < items.length; i += 1) {
        items[i].setAttribute('separator', sep);

        if (i === 0) {
          items[i].removeAttribute('data-show-separator');
        } else {
          items[i].setAttribute('data-show-separator', '');
        }
      }
    };

    onMount(() => {
      onSlotChange('default', syncItems);
      // Ensure initial slotted items are normalized once on mount.
      syncItems();
    });
    effect(syncSeparatorVar);
    effect(syncItems);

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

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.