DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
BreadcrumbTabsPaginationStepperMenu ItemToolbarMenubarNavigation MenuAccordionCollapsibleCommandBottom NavTop Header MobileTop Header DesktopSidebar
AI agentsContributing
  1. Components
  2. Accordion

Accordion

Expandable sections in a connected list or separated cards — five sizes, three variants, trigger slots for icon/description/meta, contained or plain indicators, on-color tone, heading-level control and full keyboard support.

$ pnpm add @tessinaui/ui

When to use

  • A list of related sections where people need only some. An FAQ or a settings page lets people scan the titles and open what they need.
  • Long content that need not be read in full. Collapsing it shortens the page and cuts scrolling, on a phone or in a side panel above all.
  • Sections people compare across. Set multiple so several can stay open at once, as long as a handful open together is not overwhelming.
  • Short, specific titles. A title that fits on one line scans well and hints at what is inside.
  • A familiar task for regular users. Hiding detail they already know speeds them up.

When not to use

  • For content everyone must read. Critical instructions and required legal text belong on the page, not behind a click.
  • For what a task needs to finish. Information required to complete the current step stays visible.
  • For a single inline disclosure. A lone "Show more" or "See details" is Collapsible; the accordion is the multi-section list.
  • For views that sit side by side. Peer sections people switch between are Tabs; the accordion is a vertical list only.
  • To split a sequence of questions people must answer in order. Use separate steps.
  • Nested more than two levels deep. Deeper nesting hurts findability; flatten the content instead.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import {
  AccordionRoot,
  AccordionItem,
  AccordionHeader,
  AccordionTrigger,
  AccordionPanel,
} from "@tessinaui/ui";
example.tsx
<AccordionRoot>
  <AccordionItem value="item-1">
    <AccordionHeader>
      <AccordionTrigger>What is Tessina UI?</AccordionTrigger>
    </AccordionHeader>
    <AccordionPanel>
      A code-first design system with React, TypeScript, and Tailwind CSS.
    </AccordionPanel>
  </AccordionItem>

  <AccordionItem value="item-2">
    <AccordionHeader>
      <AccordionTrigger>Does it support dark mode?</AccordionTrigger>
    </AccordionHeader>
    <AccordionPanel>
      Yes — all colours use CSS custom properties mapped to semantic tokens.
    </AccordionPanel>
  </AccordionItem>
</AccordionRoot>

Search-over-sections and sticky group headers are compositions, not props.

Examples

Default

A single-open accordion in the default ghost variant.

Sizes

Five sizes scale the trigger type, paddings, indicator optics and panel type together.

Outline

Set variant="outline" to wrap the items in a single bordered container — a connected list with flush dividers, rounded to match rounded.

Filled

variant="filled" is a card for a grouped page: the card colour in light, the raised panel tone in dark, on a page painted bg-muted dark:bg-background — the same card Card, Fieldset and Form draw for filled.

Separated

separated detaches the items into individual cards with a per-size gap — the variant chrome and rounded move onto each item and the dividers drop. Works with outline (bordered cards) and filled (grouped cards).

Trigger slots

leadingIcon, description and trailingText compose the richer rows real products use — settings rows, benefit lists, filter groups. The accessible name stays the title alone; description and meta reach the button via aria-describedby. Keep descriptions short — long button text is worse for screen-reader users than for sighted ones. The meta sits at the trailing edge, centred on the title and description; when it no longer fits beside them it wraps underneath instead of squeezing the text.

Numbered

Ordinal or step numbers ride the leadingIcon slot, so they stay out of the accessible name.

Price breakdown

The checkout expander: the collapsed row still shows the total via trailingText; the panel itemizes it.

Indicator

Switch the trigger indicator with indicator — "chevron" (default, rotates), "plus" (a + / − toggle), or "none". indicatorStyle="plain" drops the tinted circle for the bare glyph — the dominant treatment in the wild; "contained" (default) keeps the house circle. Move it to the other edge with iconSide="start" — which is an accessibility aid, not just style: it puts the state indicator next to the title, so a low-vision or magnified reader sees both without crossing the row. Don't alternate sides within one page.

Content lengths

The connected list absorbs short, medium and long panel content without the trigger rows shifting.

Multiple

Add multiple to let more than one item stay open at once.

Controlled — show / hide all

The controlled API (value + onValueChange) builds the researched "Show all sections" control: one toggle whose label flips with state.

Nested

An inner accordion is ordinary panel content. Step the heading levels with the outline (level={3} outside, level={4} inside) and stop at two levels — deeper nesting makes content hard to find. Inside a panel a ghost trigger draws its focus ring inside the row, so the panel's edge never cuts it.

Interactive panel content

Panels are free-form: controls inside them join the normal Tab order after their trigger.

On color

Inside a Surface, Accordion inherits tone="on-color": ink, dividers, hover washes and the indicator chip derive from the surface ink — no per-variant re-theming. Pass tone="on-color" yourself only on a plate something else paints; an explicit tone always wins.

Hidden until found

hiddenUntilFound keeps closed panels in the DOM behind hidden="until-found": the browser's find-in-page can match text inside a closed panel and auto-expand it, and search engines index the content. It overrides keepMounted when both are set.

Disabled Item

Disable an individual item with the disabled prop on AccordionItem — disabling never force-collapses an already-open item.

Skeleton

AccordionSkeleton renders the real root/item/trigger frame classes, so it tracks size, variant, rounded, separated, indicatorStyle and iconSide. Pass labels — the character counts of the real titles — so each bar measures what its title will measure.

RTL

Set dir="rtl" and the trigger text starts at the right while the chevron moves to the left edge, since iconSide is logical. The chevron points down and up, so it does not flip. Leave dir unset and the accordion follows the page.

API Reference

AccordionRoot props

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"Scales text, icon, and spacing
variant"ghost" | "outline" | "filled""ghost"Visual style — ghost is a divider list, outline a bordered container, filled a grouped card
rounded"none" | "sm" | "md" | "lg" | "full""lg"Corner radius (panel map — full caps at rounded-3xl). Rounds the outline/filled container, each card when separated, or the ghost trigger
separatedbooleanfalseDetach items into individual cards with a per-size gap; chrome and radius move onto each item, dividers drop
indicator"chevron" | "plus" | "none""chevron"Trigger indicator icon
indicatorStyle"contained" | "plain""contained"Tinted circle around the glyph, or the bare glyph
iconSide"start" | "end""end"Which (logical) edge the indicator sits on — start aids low-vision users
tone"default" | "on-color"inheritedon-color derives ink/borders/washes from the surface ink for coloured plates. Inherited from a wrapping <Surface>, else "default"; an explicit prop wins
panelRole"region" | "none""region"ARIA role on panels. Set "none" past ~6 panels (APG landmark guidance)
orientation"vertical" | "horizontal"—Deprecated — no effect. The accordion is vertical only; "horizontal" renders the vertical list. Still accepted so existing code type-checks
dir"ltr" | "rtl"inheritedText direction. Omitted, the accordion inherits from <html dir>; set, it lands as the DOM attribute
valueAccordionValue—Controlled array of open item values
defaultValueAccordionValue—Uncontrolled initial open values
onValueChange(value: AccordionValue) => void—Called when open items change
multiplebooleanfalseAllow multiple items open at once. Single mode always allows collapsing the last open item
disabledbooleanfalseDisables all items
loopFocusboolean—Deprecated — no effect. Base UI no longer binds arrow keys on accordion triggers (the APG contract is Enter/Space/Tab); still accepted so existing code type-checks
hiddenUntilFoundbooleanfalseUses hidden="until-found" so closed panels are discoverable by browser search. Overrides keepMounted
keepMountedbooleanfalseKeep panels in the DOM (behind hidden) when closed

AccordionItem props

PropTypeDescription
valueanyUnique identifier for this item (auto-generated if omitted)
disabledbooleanDisables this item only — never force-collapses it
onOpenChange(open: boolean) => voidCalled when this item opens or closes

AccordionHeader props

PropTypeDefaultDescription
level2 | 3 | 4 | 5 | 63Heading element rendered (h2–h6) so the accordion fits the page outline

AccordionTrigger props

PropTypeDescription
leadingIconReactNodeLeading media (icon, ordinal, small avatar). Decorative — kept out of the accessible name
descriptionReactNodeSupporting line under the title, visible collapsed. Announced via aria-describedby, never in the name. Wraps; never silently truncated
trailingTextReactNodeTrailing meta before the indicator (value, count, price), centred on the title and description; wraps under them when it no longer fits. Also aria-describedby

Renders a <button>; also accepts all standard button HTML attributes.

AccordionPanel props

PropTypeDescription
hiddenUntilFoundbooleanOverride hiddenUntilFound on this panel specifically
keepMountedbooleanOverride keepMounted on this panel specifically

Animates open/close via a CSS grid-template-rows transition (no layout-thrashing height calculations), disabled under prefers-reduced-motion.

AccordionSkeleton props

PropTypeDefaultDescription
size / variant / rounded / iconSide / indicatorStyle / separatedas Rootas RootThe same box as the real component
labelsnumber[][18, 26, 20, 23]Character counts of the real titles — bars are char-sized via skeletonTextWidth
itemsnumber3Row count; labels cycle when it exceeds them

Accessibility

Pattern: WAI-ARIA APG Accordion.

ARIA map (what actually renders — pinned by the contract tests):

ElementAttributes
Header<h2>–<h6> per level (default h3), wrapping the trigger
Trigger<button> with aria-expanded; aria-controls while its panel is open. With slots: aria-labelledby → title, aria-describedby → description + meta
Panelrole="region" + aria-labelledby → trigger. Set panelRole="none" past ~6 panels to avoid landmark proliferation (APG)

Keyboard: Enter / Space toggle the focused header; Tab / Shift+Tab move through the page order — the APG accordion contract. Arrow keys, Home and End are not bound.

AT matrix: VoiceOver: not yet run — the smoke pass (open, operate, close, listen) is still pending. NVDA and JAWS: UNTESTED — no Windows host available. The status badge stays beta until a pass is recorded.

Notes: triggers are real buttons, never link-styled text — underlined titles get mistaken for links and read as navigation. Trigger titles wrap rather than truncate — an ellipsis would hide the only text explaining the row. Panels keep their content in the normal Tab order.

Navigation MenuCollapsible

On this page

When to useWhen not to useInstallationUsageExamplesDefaultSizesOutlineFilledSeparatedTrigger slotsNumberedPrice breakdownIndicatorContent lengthsMultipleControlled — show / hide allNestedInteractive panel contentOn colorHidden until foundDisabled ItemSkeletonRTLAPI ReferenceAccordionRoot propsAccordionItem propsAccordionHeader propsAccordionTrigger propsAccordionPanel propsAccordionSkeleton propsAccessibility