DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

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.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import {
  AccordionRoot,
  AccordionItem,
  AccordionHeader,
  AccordionTrigger,
  AccordionPanel,
} from "@tessinaui/ui";
<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>

When to use — and when not to

Use an accordion to let people scan section titles and open only what they need. Don't use one for content everyone must read (critical instructions, required legal text), to split a sequence of questions people must answer in order, or nested more than two levels deep — all three measurably hurt findability (GOV.UK research, Primer).

Boundary: a single inline "Show more / See details" disclosure is the Collapsible component's job; the Accordion is the multi-section list. 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" tints every row for a denser, card-like list.

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 (tinted 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.

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 for low-vision users (Carbon). 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 GOV.UK's 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.

Interactive panel content

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

On color

On a coloured plate, tone="on-color" derives ink, dividers, hover washes and the indicator chip from the surface ink — no per-variant re-theming.

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.

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 tints each row
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""default"on-color derives ink/borders/washes from the surface ink for coloured plates
panelRole"region" | "none""region"ARIA role on panels. Set "none" past ~6 panels (APG landmark guidance)
orientation"vertical" | "horizontal""vertical"Layout axis. Horizontal is experimental — Base UI ≥1.4 deprecates accordion orientation
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). 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 verified locally (smoke: open, operate, close, listen). 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 (GOV.UK's "mistaken for links" research fix). 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.

ComponentsAction Sheet

On this page

PlaygroundInstallationUsageWhen to use — and when not toExamplesDefaultSizesOutlineFilledSeparatedTrigger slotsNumberedPrice breakdownIndicatorContent lengthsMultipleControlled — show / hide allNestedInteractive panel contentOn colorHidden until foundDisabled ItemSkeletonAPI ReferenceAccordionRoot propsAccordionItem propsAccordionHeader propsAccordionTrigger propsAccordionPanel propsAccordionSkeleton propsAccessibility