Installation
npm install @teasim/astroyarn add @teasim/astropnpm add @teasim/astrobun add @teasim/astroThe 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
Does this need JavaScript?
name.Where do the styles come from?
accordionRecipe names the classes and your entry generates them.Can I restyle it?
class, and caller classes merge last.---
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.
Green
Black
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.
Ships no stylesheet
Ships no client JavaScript
<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:
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.
import { accordionRecipe } from "@teasim/astro/recipes";
const classes = accordionRecipe();
classes.item({ className: "border-color:gray-600" });