ln-ashlar is a unified frontend library for LiveNetworks projects. It contains an SCSS CSS framework + vanilla JS components, with zero dependencies.
Overview & Philosophy
ln-accordion is applied to a parent container via data-ln-accordion wrapping multiple independent ln-toggle panels. It carries zero internal state and listens solely for bubbling ln-toggle:open events from its children. When one panel opens, it enforces mutual exclusivity by setting data-ln-toggle="close" on all open siblings—leaving all ARIA management, click handling, and smooth CSS transitions entirely to ln-toggle.
In ln-ashlar, an accordion is a pure Coordinator: a lightweight, stateless ruleset (only 38 lines of JavaScript) that sits back, listens to the DOM, and mediates between independent, highly-specialized primitives.
The Coordinator Doctrine
A coordinator never invokes instance methods directly on other components, nor does it maintain a JavaScript registry of their states. It listens for events bubbling up from the DOM, and applies decisions by writing standard HTML attributes back to the target elements. The HTML attribute is the sole contract.
The Mindset: Three Orthogonal Layers
Every accordion you see on this page is a perfect synchronization of three isolated concerns, layered gracefully on top of each other:
-
The State Primitive (
ln-toggle): Each accordion panel is an independentln-toggleinstance. It owns the binaryopen/closestate, coordinates trigger buttons, synchronizesaria-expandedandaria-controls, and handles per-panel localStorage persistence (data-ln-persist). It knows absolutely nothing about other panels or the fact that it is inside an accordion. -
The Animation Engine (CSS
.collapsible): The height transition is handled entirely in Vanilla CSS via the@mixin collapsible. It transitionsgrid-template-rowsfrom0frto1fr. No JavaScript timers, no inline element style hacks, and no framerate stuttering. -
The Coordinator (
ln-accordion): This is the glue. It lives on the wrapper element and enforces a single, exclusive rule: "When one panel opens, all other open panels in this group must close."
How the Coordination Flows
When a user clicks a trigger to open a panel:
ln-toggleintercepts the click, opens the target panel by setting its attribute todata-ln-toggle="open", and dispatches a bubbledln-toggle:openCustomEvent.- The event bubbles up to the
[data-ln-accordion]wrapper. The coordinator intercepts it, queries the immediate subtree for other elements carrying[data-ln-toggle], and setsdata-ln-toggle="close"on them. - Each closed panel's own
ln-toggleMutationObserver reacts to its attribute change, fires its standardbefore-closehooks, handles persistence updates, and collapses its content using CSS.
Why This Mindset is Superior
- Zero-configuration Multi-open: If you want multiple panels to be open at the same time, you do not need to configure an options object or change JavaScript properties. You simply remove the coordinator wrapper (
data-ln-accordion), and the individualln-togglepanels work perfectly on their own. - Unified Execution: Because the coordinator triggers a close by writing to the attribute, the closing panel runs through the exact same state machine as a direct user click. Cancelable events (
before-close) are respected, chevrons rotate back smoothly, and localStorage persistence is automatically updated. - Nestable by Nature: Since ownership is determined purely by DOM ancestry (using
.closest('[data-ln-accordion]')), accordions can be nested inside accordions infinitely. The inner coordinator handles its group, the outer coordinator handles its group, and they remain completely oblivious to each other.
For a deeper dive into the architectural rationale, see the Data Flow Architecture, the ln-accordion README, and the architecture deep-dive.
Anatomy
The minimal shape. Every working accordion is a variation on this skeleton.
<ul data-ln-accordion>
<li>
<header data-ln-toggle-for="p1">
Section 1
<svg class="ln-icon ln-chevron"><use href="#ln-icon-arrow-down"></use></svg>
</header>
<section id="p1" data-ln-toggle="open" class="collapsible">
<article class="collapsible-body"><p>Content 1.</p></article>
</section>
</li>
<li>
<header data-ln-toggle-for="p2">
Section 2
<svg class="ln-icon ln-chevron"><use href="#ln-icon-arrow-down"></use></svg>
</header>
<section id="p2" data-ln-toggle class="collapsible">
<article class="collapsible-body"><p>Content 2.</p></article>
</section>
</li>
</ul>
The wrapper — <ul data-ln-accordion>
The wrapper is a plain marker — the attribute takes no value. <ul> because an accordion is, semantically, an unordered list of collapsible items. The wrapper is the listener for the bubbled ln-toggle:open event and the dispatch element for ln-accordion:change.
Each item — <li> with a trigger header and a panel
The trigger declares which panel it controls via data-ln-toggle-for="p1"; the panel declares its own id="p1". The pairing is by ID, not by DOM proximity — the <li> just keeps them visually grouped. The chevron icon inside the header is plain decoration; ln-toggle's default CSS rotates any .ln-chevron 180° when the controlled panel is open, driven by aria-expanded on the trigger.
The panel — <section data-ln-toggle class="collapsible">
Three concerns layered on one element. id="p1" is the lookup target for the trigger. data-ln-toggle creates an ln-toggle instance — its value ("open" or anything else) is the open/closed state. class="collapsible" is the CSS animation: grid-template-rows: 0fr by default, 1fr when the element carries .open (added by ln-toggle from its attribute).
Element type is <section>. It cannot be <main> — the HTML spec allows only one <main> per page.
The body — <article class="collapsible-body">
The collapse animation requires the parent (.collapsible) to have zero padding so it can animate to zero height cleanly. Padding and margins live one level deeper, on the body child. Putting padding on .collapsible directly leaves a thin always-visible strip when collapsed.
What state goes where
"Is this panel open?" lives on the panel's data-ln-toggle attribute (owned by ln-toggle). "Is the chevron rotated?" lives on the trigger's aria-expanded (owned by ln-toggle). "Did opening this panel close the others?" is transient — handled by ln-accordion reactively, with no stored state. The accordion's instance has two fields: dom and _onToggleOpen. Nothing else.
Attributes reference
ln-accordion's entire attribute surface is one marker. Every other attribute in the markup is owned by ln-toggle or by the CSS mixin.
| Attribute | On element | Owned by | Description |
|---|---|---|---|
data-ln-accordion | wrapper | ln-accordion | Marks the wrapper. Creates the coordinator instance. |
data-ln-toggle | panel | ln-toggle | Creates an ln-toggle instance. Value "open" = open; anything else = closed. |
data-ln-toggle-for="id" | trigger | ln-toggle | References the panel by ID. Click toggles it. |
data-ln-persist | panel | ln-toggle | Persist this panel's state in localStorage. Per-panel. |
id | panel | HTML | Required so the trigger's data-ln-toggle-for can resolve it. |
class="collapsible" | panel | CSS mixin | Grid-rows collapse animation. |
class="collapsible-body" | body | CSS mixin | Holds padding/margins so the parent can collapse to zero. |
Example — Basic accordion
Three panels, one open by default. Try clicking each header — opening one closes the previously-open one. Open DevTools and watch data-ln-toggle flip on each <section>; that single attribute is the source of truth.
-
What is ln-ashlar? -
How to use it? You can use it as an npm package, via git submodule, or directly with
<link>and<script>tags.import 'ln-ashlar'; import 'ln-ashlar/dist/ln-ashlar.css'; -
Does it have dependencies? ln-ashlar is fully independent. All JS components are written in vanilla JavaScript using the IIFE pattern.
Example — Headers with rich content
Triggers are full <header> elements. They can hold any markup — headings, badges, multi-line content. The whole header is the click target. Below: each panel uses <h4> in the trigger and <ul> with the prose class for the body content.
-
User Documentation
-
CSS Components
-
JS Components
Example — Nested accordions
Nested accordions are supported. Each accordion only manages toggles whose nearest [data-ln-accordion] ancestor is itself — opening an inner panel does not affect the outer panel, and opening an outer panel does not touch the inner accordion's open/closed state. Try it: open Inner 2 and watch Outer 1 stay open; close Outer 1 and re-open it to see the inner accordion's previous state preserved.
-
Outer 1 — contains a nested accordion Inner accordion below — opening Inner 1 / Inner 2 does not affect Outer 1.
-
Inner 1 Inner 1 starts open. Opening Inner 2 closes this. Opening Outer 2 leaves this state alone.
-
Inner 2 Inner 2 starts closed.
-
-
Outer 2 Opening Outer 2 closes Outer 1 (which collapses the inner accordion with it) but does not change the inner accordion's open/closed state — re-opening Outer 1 restores the inner accordion's previous state.
Events
The accordion dispatches one event per panel-open. It bubbles, is non-cancelable, and detail.target is the panel.
| Event | Bubbles | Cancelable | Detail | Dispatched when |
|---|---|---|---|---|
ln-accordion:change | yes | no | { target: HTMLElement } | After every ln-toggle:open is processed (siblings closed; :change dispatched even when nothing needed closing) |
document.addEventListener('ln-accordion:change', function (e) {
console.log('Active panel:', e.detail.target.id);
});
The accordion does not listen to ln-toggle:close. Closing the only-open panel just leaves the accordion fully collapsed — that's intentional.
API
There is no open(panelId) or close(panelId) method. To programmatically change the active panel, set data-ln-toggle="open" directly on the target panel — ln-toggle's observer will apply the open state, the event will bubble up, and ln-accordion will close the previously-open siblings.
// Open panel2 — accordion sees the bubbled ln-toggle:open and closes panel1
document.getElementById('panel2').setAttribute('data-ln-toggle', 'open');
// Tear down the coordinator (children remain intact)
const acc = document.querySelector('[data-ln-accordion]');
acc.lnAccordion.destroy();
Common mistakes
1. Padding on .collapsible directly
The collapse animation works by transitioning grid-template-rows from 0fr to 1fr. Padding on the parent leaves a visible strip when collapsed. Padding always goes on .collapsible-body.
<!-- WRONG -->
<section data-ln-toggle class="collapsible" style="padding: 1rem;">…</section>
<!-- RIGHT -->
<section data-ln-toggle class="collapsible">
<article class="collapsible-body">…</article>
</section>
3. Putting data-ln-toggle on the trigger header
data-ln-toggle creates a panel; data-ln-toggle-for creates a trigger. They are mutually exclusive — one element is one or the other. Putting both on the header turns the header into a panel that controls itself.
<!-- WRONG -->
<li>
<header data-ln-toggle data-ln-toggle-for="p1">Section 1</header>
<section id="p1" class="collapsible">…</section>
</li>
4. Panels living outside the accordion subtree
The pairing between trigger and panel is by ID, so ln-toggle works fine across the wrapper boundary. But the accordion's listener is scoped to its own subtree — bubbled ln-toggle:open events from outside don't reach it, so the single-open rule won't fire. Keep panels inside the accordion's DOM.
<!-- WRONG — panels outside the accordion -->
<ul data-ln-accordion>
<li><header data-ln-toggle-for="p1">Section 1</header></li>
</ul>
<section id="p1" data-ln-toggle class="collapsible">…</section>
Related
ln-toggle— every panel in the accordion is anln-toggleinstance. Open/close API, persistence, cancelable events and aria sync all come from there.@mixin collapsible— thegrid-template-rowsanimation..collapsibleis the parent (zero padding);.collapsible-bodyis the padded child.@mixin accordion— bordered-card chrome and chevron rotation. Already applied to[data-ln-accordion]by default; re-apply on a custom selector for the look without single-open coordination.- Architecture deep-dive:
js/ln-accordion/README.md— internals, lifecycle, design rationale. - Cross-component principles:
docs/architecture/data-flow.md— attributes-as-contract, event-driven coordination, MutationObserver discipline.