Breadcrumb

Trail navigation that collapses the middle into an ellipsis when the path gets long. The full trail lives in state.trail; visible() derives the rendered segments. Clicking the ellipsis expands the trail in place.

Live preview

Markup

<nav class="breadcrumb" data-component="breadcrumb-demo" aria-label="Breadcrumb">
  <template data-each="visible()" data-key="key">
    <span data-if="item.ellipsis">
      <button class="crumb-ellipsis" @click="expand">…</button>
      <span class="crumb-sep" aria-hidden="true">›</span>
    </span>
    <span data-if="!item.ellipsis && !item.current">
      <a data-bind="href:item.href" data-text="item.label"></a>
      <span class="crumb-sep" aria-hidden="true">›</span>
    </span>
    <span class="crumb-current"
          data-if="item.current"
          aria-current="page"
          data-text="item.label"></span>
  </template>
</nav>

Definition

Micra.define('breadcrumb-demo', {
  state: {
    trail: [
      { label: 'Workspace', href: '/ws' },
      { label: 'Projects',  href: '/ws/projects' },
      { label: 'Atlas',     href: '/ws/projects/atlas' },
      { label: 'Settings',  href: '/ws/projects/atlas/settings' },
      { label: 'Billing',   href: '/ws/projects/atlas/settings/billing' },
    ],
    showAll: false,
    maxSegments: 4,  // collapse if trail exceeds this
  },

  visible() {
    const { trail, showAll, maxSegments } = this.state
    const len = trail.length
    const tail = (item, i) => ({ ...item, current: i === len - 1, key: 'c-' + i })

    if (showAll || len <= maxSegments) {
      return trail.map(tail)
    }
    // Show: first + ellipsis + last 2
    return [
      tail(trail[0], 0),
      { ellipsis: true, key: 'ellipsis' },
      tail(trail[len - 2], len - 2),
      tail(trail[len - 1], len - 1),
    ]
  },

  expand() { this.state.showAll = true },

  setShort() {
    this.state.showAll = false
    this.state.trail = [
      { label: 'Workspace', href: '/ws' },
      { label: 'Projects',  href: '/ws/projects' },
      { label: 'Atlas',     href: '/ws/projects/atlas' },
    ]
  },

  setLong() {
    this.state.showAll = false
    this.state.trail = [
      { label: 'Workspace', href: '/ws' },
      { label: 'Projects',  href: '/ws/projects' },
      { label: 'Atlas',     href: '/ws/projects/atlas' },
      { label: 'Settings',  href: '/ws/projects/atlas/settings' },
      { label: 'Billing',   href: '/ws/projects/atlas/settings/billing' },
    ]
  },
})

Integration

  • Trail is just data. Render server-side, load from the router, or hydrate from a fetch — replace state.trail and the breadcrumb updates.
  • Collapse threshold (maxSegments) is state, so you can change it per viewport — pair with a window.matchMedia listener in onCreate for responsive collapse.
  • ARIA. aria-label="Breadcrumb" on the <nav> and aria-current="page" on the last segment are the two attributes assistive tech actually consumes.
  • Keys. Each segment carries a key (the index or 'ellipsis') so keyed data-each survives expand/collapse without unnecessary DOM churn.

Pitfalls

  • Don't store collapsed as a state field redundant with showAll — derive whatever you need from the existing flag.
  • Don't put the chevron inside the <a> — it then becomes a click target for the link, which surprises users who land on a pixel between segments.
  • Don't compute visible() by mutating the trail. The { ...item, current } spread creates fresh objects so the source array stays untouched.