Skip to content

Accordion

Disclosure rows built on the native details element, with the browser owning the open state.

View as Markdown

Installation

npm install @teasim/astro

The open state is <details> and <summary>, so a row opens before any JavaScript runs and keeps working if none ever does. A delegated runtime binds on top to add what the element has no answer for — arrow-key roving focus, multiple, defaultValue, and an accordion:change event. There is no client directive to write; it binds itself and survives a view transition.

Preview

Preview

Does this need JavaScript?

No. The disclosure is the browser’s, and so is the exclusive-group behaviour when items share a name.

Where do the styles come from?

Your application’s TeaCSS build. The package ships no stylesheet — accordionRecipe names the classes and your entry generates them.

Can I restyle it?

Every part takes class, and caller classes merge last.
astro
---
import { Accordion, AccordionContent, AccordionItem, AccordionSummary } from "@teasim/astro/accordion";
---

<Accordion>
  <AccordionItem name="faq" open>
    <AccordionSummary><h3>Does this need JavaScript?</h3></AccordionSummary>
    <AccordionContent>No. The disclosure is the browser's.</AccordionContent>
  </AccordionItem>
  <AccordionItem name="faq">
    <AccordionSummary><h3>Where do the styles come from?</h3></AccordionSummary>
    <AccordionContent>Your application's TeaCSS build.</AccordionContent>
  </AccordionItem>
</Accordion>

Examples

One at a time

Give sibling Items the same non-empty name and the browser closes the open one when another opens. The group name is document-wide, so a page with two independent accordions needs two names.

Exclusive group

Green

Two minutes, water just off the boil.

Black

Three minutes, water at a rolling boil.

Open at most one member of a named group initially — two Items sharing a name and both carrying open is a state the browser has to resolve, and it closes one of them for you.

Several at once

Dropping name is not enough. multiple defaults to false, and on bind the runtime keeps the first open Item and closes the rest — so two Items that both carry open render as one open row. Pass multiple on the root to keep them all.

multiple

Ships no stylesheet

The application’s TeaCSS build generates every rule.

Ships no client JavaScript

For this family the open state is native; the runtime only adds focus and events.
astro
<Accordion multiple>
  <AccordionItem open>…</AccordionItem>
  <AccordionItem open>…</AccordionItem>
</Accordion>

The two mechanisms are independent and both apply. name is the browser’s, and it works with scripting off; multiple is the runtime’s, and it wins on bind. Setting multiple while siblings share a name gets you the browser’s exclusivity anyway — use one or the other, not both.

API reference

Four components, each rendering one native element.

Component Renders Own props
Accordion <div> multiple, defaultValue, collapsible, orientation, loopFocus
AccordionItem <details> value — plus native open and name
AccordionSummary <summary> none
AccordionContent <div> none

Accordion

Prop Type Default Notes
multiple boolean false Several Items open at once
defaultValue string | string[] Which value starts open; an array joins with commas
collapsible boolean true The last open Item may close
orientation "horizontal" | "vertical" "vertical" Which arrow keys move focus
loopFocus boolean true Focus wraps at either end

AccordionItem

Prop Type Default Notes
value string Identifies the Item to defaultValue
open boolean false Native <details> attribute
name string Native group name; siblings sharing one are mutually exclusive

AccordionSummary must be the first direct element child of its Item — that is a <details> requirement, not this component’s. AccordionContent renders a plain <div> and does not force a region landmark.

Each component owns aria-controls, aria-disabled, aria-expanded and role, and AccordionSummary owns tabindex as well; passing any of them is ignored rather than merged. Everything else native passes through.

How the props reach the runtime

The five root props are not read by the server half. Accordion writes each as a data-* attribute — data-multiple, data-default-value, data-collapsible, data-orientation, data-loop-focus — and the runtime reads them from the DOM on bind. That is why the markup is complete before any script runs — the options travel as markup, not as a hydration payload.

The runtime itself is not public API. @teasim/astro/accordion exports the four components and nothing else; the browser half is loaded by the component’s own <script>, which is the single edge between the two halves. Drive a bound Accordion through events instead:

ts
const root = document.querySelector('[data-slot="accordion/root"]');

// Listen: fires on the root whenever the open set changes.
root.addEventListener("accordion:change", (event) => {
  event.detail.value; // string[] — the open Items' values
});

// Drive: dispatch on the root, not on an Item.
root.dispatchEvent(new CustomEvent("accordion:set", { detail: { value: ["shipping"] } }));

Both carry { value } rather than a bare array, and accordion:set is ignored unless its target is the root itself. An Item needs a value for either direction to name it.

accordionRecipe

accordionRecipe exposes root, item, summary, and content. It lives on @teasim/astro/recipes, never on the family subpath, so rendering an Accordion never pulls a recipe in behind it.

ts
import { accordionRecipe } from "@teasim/astro/recipes";

const classes = accordionRecipe();
classes.item({ className: "border-color:gray-600" });