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.