Accordion

Expand / collapse sections rendered from data. The expanded set lives in an array of ids — switching between single-expand and multi-expand is a one-line change in toggle(). isOpen() is a method, never a state mirror.

Live preview

Markup

<div data-component="accordion-demo" class="accordion">
  <template data-each="items" data-key="id">
    <div class="accordion-item">
      <button class="accordion-trigger"
        @click="toggle"
        data-bind="data-id:item.id, aria-expanded:isOpen(item.id)">
        <span data-text="item.title"></span>
        <svg data-class="open:isOpen(item.id)">…chevron…</svg>
      </button>
      <div class="accordion-body"
           data-if="isOpen(item.id)"
           data-text="item.body"></div>
    </div>
  </template>
</div>

Definition

Micra.define('accordion-demo', {
  state: {
    multi: false,           // flip to true for multi-expand
    open:  ['shipping'],    // ids of currently expanded items
    items: [
      { id: 'shipping', title: 'When will my order ship?',     body: 'Within 24h on business days.'        },
      { id: 'returns',  title: 'What is your returns policy?', body: '30 days, free return shipping.'      },
      { id: 'support',  title: 'How do I contact support?',    body: 'Reply to your order confirmation email.' },
    ],
  },

  // derived — never store this in state
  isOpen(id) {
    return this.state.open.includes(id)
  },

  toggle(e) {
    const id = e.currentTarget.dataset.id
    const wasOpen = this.isOpen(id)

    if (this.state.multi) {
      this.state.open = wasOpen
        ? this.state.open.filter(x => x !== id)
        : [...this.state.open, id]
    } else {
      this.state.open = wasOpen ? [] : [id]
    }
  },
})

Integration

  • Items come from data. Drive the accordion with an array of { id, title, body } objects — render server-side, or load via this.fetch() in onCreate.
  • Multi vs single is one state field. Default is single-expand (clicking another closes the first). Set multi: true and clicking another adds it to the open set instead.
  • Chevron animation is CSS: the .icon-chev.open rule rotates 180°. data-class toggles the class additively — the icon's other classes are preserved.
  • ARIA. aria-expanded on each trigger is bound to isOpen(item.id) so screen readers stay in sync.

Pitfalls

  • Don't store an openMap: { [id]: bool } object — nested writes (openMap.shipping = true) are invisible to the proxy. The array-of-ids pattern is replace-only and stays reactive.
  • Don't compute openCount / expanded as a state field — derive with a method (openCount() { return this.state.open.length }) so it can't drift.
  • Don't use data-show for bodies if they're large — content stays in the DOM and the page grows. Use data-if so only expanded bodies are rendered.