Accordion
import { Accordion, useAccordionItem } from '@tangible/ui';
Interactive docs and examples: Accordion in Storybook.
Accordion props
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
||
type |
"multiple" | "single" |
||
className? |
string |
||
collapsible? |
boolean |
Allow closing all items (otherwise one stays open) | |
defaultValue? |
string | string[] |
Uncontrolled: initially open item | |
onValueChange? |
((value: string | undefined) => void) | ((value: string[]) => void) |
Callback when open item changes. Receives undefined when all items are closed (collapsible mode) — store it as value ?? null if your state is string | null. |
|
value? |
string | string[] |
Controlled: currently open item value. null means "controlled, nothing open". Controlled vs uncontrolled is decided once, at mount, from whether value is undefined — so pass null (not undefined) for "all closed", and if undefined does leak back in later (onValueChange reports it on collapse), it reads as "nothing open" rather than switching modes. |
|
variant? |
"card" | "flush" |
'card' |
Visual treatment. card (default): Bordered cards with gap between items. flush: Items share borders, no gap, no radius — denser layout. |
Accordion.Item
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
||
value |
string |
Unique identifier for this item (required) | |
className? |
string |
||
disabled? |
boolean |
false |
Prevent interaction |
headingLevel? |
2 | 3 | 4 | 5 | 6 |
Wrap trigger in heading element (h2–h6). Omitting this reduces discoverability for screen reader users who navigate by headings (NVDA/JAWS H key). Only omit when the accordion is already inside an appropriate heading context. |
Accordion.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label? |
string |
Accessible label for the trigger. Required when children is empty (e.g. icon-only triggers). | |
asChild? |
boolean |
false |
When true, merges trigger props (aria attributes, event handlers, keyboard navigation) onto the child element instead of rendering a built-in button. The child must be a single React element that accepts ref and event handlers. The built-in chevron indicator is not rendered in asChild mode. |
children? |
ReactNode |
||
className? |
string |
||
unstyled? |
boolean |
false |
asChild only. Skip the tui-accordion__trigger class — the full-width row paint (flex, width 100%, padding, hover background, inset focus ring) — and leave the child's own styling alone. The behaviour hook (data-tui-accordion-trigger, data-state, ARIA, handlers) still lands. For a child that is already a styled control (an IconButton), not for a custom row that wants the accordion's look. |
Accordion.Panel
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
||
className? |
string |
||
landmark? |
boolean |
false |
Render panel as a landmark region (role="region"). Default false to avoid landmark pollution with many panels. |
Hooks
useAccordionItem
useAccordionItem(): AccordionItemState
Returns:
| Member | Type | Description |
|---|---|---|
value |
string |
The item's value. |
isOpen |
boolean |
Whether the item is currently open. |
disabled |
boolean |
Whether the item is disabled — a secondary control should disable too. |
triggerId |
string |
id of the item's trigger (the built-in one, or the asChild element). |
panelId |
string |
id of the item's panel — the value for a secondary control's aria-controls. |
toggle |
() => void |
Toggle this item, honouring type/collapsible and disabled. |
Examples
-
Token Customisation: Demonstrates the token API for fine-grained control over trigger vs panel appearance. All customisation is done via CSS custom properties on a wrapping element — no extra props needed.
Tokens shown here:
--tui-accordion-trigger-bg/--tui-accordion-trigger-padding--tui-accordion-panel-bg/--tui-accordion-panel-padding--tui-accordion-panel-spacing(set to0to remove block breathing room)--tui-accordion-divider
-
No Panel Block Spacing: Panel with
--tui-accordion-panel-spacing: 0removes the top padding and bottom pseudo-element, useful when panel content provides its own padding (e.g. a padded card or list with built-in spacing). -
Complex Header (Sibling Controls): Demonstrates the correct pattern for accordion headers with interactive controls. The Trigger is a plain button (just the chevron) placed as a sibling to move handles, editable titles, and action buttons in a flex row. No nesting, no role hacks — each element is independent.
Consumers can optionally stretch the trigger's click area across the full row with a
::afterpseudo-element if desired.What to test:
- Chevron button toggles the panel
- Action buttons log to the console without toggling
- Title edit button enters edit mode without toggling
- Move handle buttons work without toggling
- Tab focuses each control independently in DOM order
- Arrow Up/Down on the chevron navigates between accordion triggers
- Disabled item is skipped during keyboard nav
-
asChild IconButton + Secondary Control: The repeater-row shape: the trigger is an
IconButtonviaasChild unstyled, so it keeps its own sizing and focus ring instead of inheriting the full-width row paint (tui-accordion__trigger) — the behaviour hook (data-tui-accordion-trigger,data-state, ARIA, Enter/Space/arrow keys) still lands. Being a native<button>, Enter and Space toggle exactly once: the trigger leaves activation to the browser rather than synthesising a second one. An Edit/Close link beside it toggles the same item throughuseAccordionItem(), with its ownaria-expanded/aria-controls. The accordion is controlled withvalue: string | null—nullis "nothing open", not "uncontrolled". Note the chevron rotation belongs to the paint class, so an unstyled trigger shows open/closed by swapping the icon itself.
Other stories: Playground, Basic, Single (Non-Collapsible), Multiple, With Step Indicators, Course Syllabus, Controlled, Disabled Items, Keyboard Navigation, Flush Variant, Without Headings (interactive page).
Component API tokens
Read via fallback, never declared. Set one on an ancestor to restyle every instance under it.
--tui-accordion-bg--tui-accordion-bg-open--tui-accordion-fg--tui-accordion-border--tui-accordion-divider--tui-accordion-radius--tui-accordion-padding--tui-accordion-trigger-padding--tui-accordion-trigger-bg--tui-accordion-trigger-hover--tui-accordion-panel-padding--tui-accordion-panel-bg--tui-accordion-panel-spacing--tui-accordion-gap--tui-accordion-indicator