Skip to contentAsheeUIAsheeUI

Accordion

A column of sections a reader opens one at a time, or several at once.

An accordion folds a long answer into a list of questions. Each row is a trigger and the panel under it opens in place, so a reader scans the questions first and reads only the answer they came for, a support page, a group of settings, the line-item detail under an order.

What separates it from a tab set is that the sections are not alternatives to each other: they belong in one column, closing one is as reasonable as opening it, and nothing about the page moves to a different place. The one mode it adds is whether more than a single section may stand open.

Usage

import { Accordion } from "@asheeui/web";

<Accordion
  items={[
    {
      id: "invoices",
      title: "Where are last year's invoices?",
      content: "Under Billing, then Invoices. Every year stays available.",
    },
    {
      id: "seats",
      title: "Can I add a seat mid-month?",
      content: "Yes, and the difference is charged at the next renewal.",
    },
  ]}
  defaultValue="invoices"
/>

An item is an id, a title and the content its panel reveals, plus a subtitle under the title, an icon before it and disabled. The id is the key the open state is compared against; an item without one is keyed by its position, which is what a list with no ids means.

Expansion and state

allowMultiple is the whole of the mode, and it is registered as false: opening a section closes the one that was open. With it stated, several sections stay open at once. The mode changes nothing about the trigger, the panels or the announcement, only how many keys the state may hold.

defaultValue is where an uncontrolled accordion starts, and value with onValueChange is the controlled pair. A controlled accordion renders exactly the keys it is given and reports every change; an uncontrolled one keeps its own and reports them the same way, so a page can start uncontrolled and take over later without changing anything else. onValueChange always receives the complete set of open keys as an array, whichever shape stated it, so a page has one thing to store.

The single-open rule is applied when the state changes rather than when it renders, which is why a controlled value holding two keys draws two open panels even while allowMultiple is false: the component renders the state it is handed, and the next press is what folds it back to one.

Treatments

Four variants decide the surface. bordered groups the items into one outlined container with dividers; separated gives each item its own outlined block with space between them; ghost draws transparent rows over hairlines; flush drops the container, the borders and the dividers for a tight bare stack.

size sets the header's padding and the type scale, in the same three steps the fields use. radius rounds the container where the variant has one and rounds the items in separated and ghost; flush has no surface left to round. disableAnimation opens and closes without the transition, the value a page states for a screenshot test, or for a recording in which a section moving is noise rather than information. It is a cascade value like the rest, so components.accordion.disableAnimation retunes every accordion at once.

A closed panel is collapsed and inert rather than unmounted, so a form inside it keeps whatever the reader had typed when they folded it away.

Props

size (md) and allowMultiple (false) are registered, so components.accordion states the density and the mode once for the application.

variant and radius are deliberately not registered: variant resolves from the application's defaultVariant and then from the accordion's own separated, and radius from the application's defaultRadius and then md. The framework's default configuration states a faded variant, which is a button treatment rather than an accordion one, so an unconfigured application resolves to separated, the same treatment the component documents.

items, defaultValue, value, onValueChange, expandIcon and the three class-name overrides are read from the instance: the sections, which of them start open, and the node that marks them are statements about one screen. disableAnimation is the exception, it is a statement about the design, so it resolves through the cascade.

Accessibility

  • Every header is a real <button type="button"> carrying aria-expanded and aria-controls, and every panel is a <section> labelled by its header. A reader is told the name of the region as well as whether it is open, and both identifiers are generated from one id, so they cannot drift apart.
  • A closed panel is collapsed to zero height and marked inert, which takes its content out of the tab order and out of the accessibility tree without unmounting it. Opening the panel restores both, with whatever the content had kept.
  • The default indicator is aria-hidden, but expandIcon and item.icon are drawn inside the trigger, so a node that carries text joins the header's accessible name. Hide a decorative icon, or give a meaningful one its text alternative.
  • ArrowDown and ArrowUp move between headers, Home and End go to the first and last, and the movement wraps. Moving focus never opens a panel: the arrows are how a reader gets to a header, and the press is how it opens. A disabled header is skipped by the arrows and remains reachable by Tab.
  • A disabled item carries aria-disabled rather than the native disabled attribute, so it stays focusable and findable, a reader can reach it and be told why it will not open, while the press is refused. (A Button follows the same rule.)