JavaScript Accordion
@lemonadejs/accordion · ✓ 9 contract checks · framework-agnostic · zero dependencies
<Accordion /> — expansion panels on the v6 contract model.
Panels come from a data array ({ title, content?, disabled? }):
- content is trusted TEXT (v6 strings are always text, never HTML); rich panel bodies come from the render prop: (item, index) => html view
- every panel body is created once and KEPT ALIVE across toggles — the collapse is a grid-template-rows (0fr->1fr) transition driven by data-open (CSS), never an unmount, so content state (inputs, nested components) survives open/close cycles; closed bodies are inert (out of the Tab order and the accessibility tree)
- exclusive by default (a controlled accordion group): bind is the expanded INDEX, -1/null = all closed, opening one closes the other
- multiple: bind becomes an ARRAY of open indices — each panel toggles independently
Headers are real <button>s: native Enter/Space toggling, native disabled
semantics; ArrowUp/ArrowDown walk focus between enabled headers. Each
body is a labelled ARIA region (header aria-controls ⇄ body
aria-labelledby); panels are keyed by item identity so kept-alive
bodies move with their item when the options array changes.
Bound state semantics (the v6 protocol): expanded.set() on user toggles fires onchange(expanded, previous); external writes through the bound state stay silent.
Example
import { html } from 'lemonadejs';
import Accordion from '@lemonadejs/accordion';
const App = (props, { state }) => {
const expanded = state(0);
const faq = [
{ title: 'How long does shipping take?', content: 'Orders ship within 24 hours and arrive in 2 to 5 business days.' },
{ title: 'Can I return an item?', content: 'Yes — returns are free within 30 days of delivery.' },
{ title: 'Do you ship internationally?', content: 'We ship to 40+ countries; duties are calculated at checkout.' },
{ title: 'Wholesale pricing', content: 'Available on request.', disabled: true },
];
return html`<div>
<${Accordion} options="${faq}" bind="${expanded}" />
<p>Open panel: <b>${() => (expanded.value >= 0 && expanded.value !== null ? faq[expanded.value].title : 'none')}</b></p>
</div>`;
};Installation
npm install @lemonadejs/accordion
import Accordion from '@lemonadejs/accordion';
import '@lemonadejs/accordion/style.css';
Three deployment forms, one component:
html`<${Accordion} />` // by value (no registration)
setComponents({ Accordion }); // then <Accordion /> by name anywhere
createWebComponent(Accordion); // <lm-accordion> in plain HTML/any framework
Props
Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.
| Prop | Type | Default | Description |
|---|---|---|---|
bind | number | — | Two-way bound value. .set() fires onchange; plain assignment is silent. |
options | array | — | |
render | function | — | |
multiple | boolean | false |
Events
All event names are lowercase (the platform convention — LJS-305 warns otherwise).
onchange
Styling
All classes follow the lm-accordion-* convention; visual variants are data-*
attributes on the root. Override freely — there is no styling engine to fight.
Contract
The machine-readable schema ships with the package:
import contract from '@lemonadejs/accordion/contract.json';
verify.json carries the conformance proof produced by verify(Accordion).
Looking for the v5 plugin? See the archived v5 documentation.