|Kit

Accordion

A stack of expandable sections on Ark UI's Accordion machine. Content animates via the kit's own zero-JS Collapse primitive, not Ark's JS-measured height.

Accordion is a stack of expandable sections: each item is a heading wrapping a trigger button, and clicking it reveals that item's panel. Content never animates via Ark's own JS-measured height — the same category of runtime-measurement quirk this kit already worked around for Slider's thumbSize — instead every panel renders through Collapse, this kit's own zero-JS, CSS-grid-rows animation primitive.

import { Accordion } from '@42-central/ui-react/accordion';

<Accordion
  data={[
    { value: 'shipping', label: 'Shipping', content: 'Orders ship within 2 business days.' },
    { value: 'returns', label: 'Returns & refunds', content: 'Returns accepted within 30 days.' },
  ]}
/>

Playground

Component

Loading preview

Props

Code

<Component
  variant="default"
  size="md"
  radius="sm"
  chevronPosition="end"
  multiple={false}
  collapsible={false}
/>

Items

data is a flat array of { value, label, content, disabled?, icon? } — each section needs its own content, so (unlike SegmentGroup) there's no bare-string shorthand.

<Accordion
  data={[
    { value: 'shipping', label: 'Shipping', content: <Shipping /> },
    { value: 'returns', label: 'Returns & refunds', content: <Returns /> },
    { value: 'payment', label: 'Payment methods', content: <Payment />, disabled: true },
  ]}
/>

Each item also accepts an optional testId, defaulting to `accordion-${value}` — the matching content panel gets `${testId}-content`. See the Testing guide. The compound Accordion.Item / Accordion.ItemTrigger / Accordion.ItemContent / Accordion.Root parts each take their own testId prop directly, for the same reason.

Variants

Two treatments — a different vocabulary than Button's own filled/light/outline/subtle. default is a single bordered list with dividers between items; separated lays each item out as its own bordered card with a gap between them, its border deepening to a solid neutral ink while open. radius (ignored by default) rounds the separated cards.

Ships in 2 days.

Accepted within 30 days.

Ships in 2 days.

Accepted within 30 days.

Code

<Accordion variant="default" data={[{ value: 'a', label: 'Shipping', content: 'Ships in 2 days.' }, { value: 'b', label: 'Returns', content: 'Accepted within 30 days.' }]} />
<Accordion variant="separated" data={[{ value: 'a', label: 'Shipping', content: 'Ships in 2 days.' }, { value: 'b', label: 'Returns', content: 'Accepted within 30 days.' }]} />

Sizes

Five sizes — xs through xl — matching the rest of the family. Sizing lives on the triggers and panels.

Ships in 2 days.

Ships in 2 days.

Ships in 2 days.

Code

<Accordion size="xs" data={[{ value: 'a', label: 'Shipping', content: 'Ships in 2 days.' }]} defaultValue={['a']} />
<Accordion size="md" data={[{ value: 'a', label: 'Shipping', content: 'Ships in 2 days.' }]} defaultValue={['a']} />
<Accordion size="xl" data={[{ value: 'a', label: 'Shipping', content: 'Ships in 2 days.' }]} defaultValue={['a']} />

Multiple

By default only one section can be open at a time. Pass multiple to let any number stay expanded together.

Orders ship within 2 business days. Tracking is emailed once your package leaves the warehouse.

Unused items can be returned within 30 days for a full refund, minus the original shipping cost.

We accept all major cards, PayPal, and bank transfer for orders over $500.

Collapsible

In single mode (the default), the one open section can't be closed by re-clicking its own trigger — some other section must be opened instead. Pass collapsible to allow closing it directly, leaving every section collapsed.

Orders ship within 2 business days. Tracking is emailed once your package leaves the warehouse.

Unused items can be returned within 30 days for a full refund, minus the original shipping cost.

We accept all major cards, PayPal, and bank transfer for orders over $500.

Chevron

chevronPosition ("start" | "end", default "end") is a logical position, not a physical left/right — it flips correctly under dir="rtl" with no extra CSS (the same convention Divider's labelPosition uses).

Orders ship within 2 business days. Tracking is emailed once your package leaves the warehouse.

Unused items can be returned within 30 days for a full refund, minus the original shipping cost.

We accept all major cards, PayPal, and bank transfer for orders over $500.

Pass chevron to swap the icon entirely, or chevron={null} to hide it — there's no separate flag to keep the default icon but disable its open/close rotation.

import { ChevronDown } from 'lucide-react';

<Accordion chevron={<ChevronDown />} data={SECTIONS} />
<Accordion chevron={null} data={SECTIONS} />

Disabled

A single item can be disabled via { value, label, content, disabled: true } — it's skipped by keyboard navigation and can't be opened. Pass disabled on Accordion itself to disable every item at once.

Available to everyone.

Available to everyone.

Admins only.

Compound API

Drop to the composable parts for anything the flat data array can't express — custom trigger content, interleaved non-item elements, or driving the value from your own state. variant / size / radius / chevronPosition / chevron / order all live on Accordion.Root — there's no separate List part the way Tabs has one, since the root itself is the list — and cascade to every item through context.

'use client';
import { Accordion } from '@42-central/ui-react/accordion';

<Accordion.Root variant="separated" defaultValue={['shipping']}>
  <Accordion.Item value="shipping">
    <Accordion.ItemTrigger>Shipping</Accordion.ItemTrigger>
    <Accordion.ItemContent>Orders ship within 2 business days.</Accordion.ItemContent>
  </Accordion.Item>
  <Accordion.Item value="returns">
    <Accordion.ItemTrigger>Returns & refunds</Accordion.ItemTrigger>
    <Accordion.ItemContent>Returns accepted within 30 days.</Accordion.ItemContent>
  </Accordion.Item>
</Accordion.Root>

Controlled

Drive the value from state with the same value / onChange pair either way: the data-driven form takes them as top-level props, and the compound Accordion.Root accepts them too — it flattens Ark's own onValueChange into onChange for you, so onValueChange isn't a prop on either form.

'use client';
import { useState } from 'react';
import { Accordion } from '@42-central/ui-react/accordion';

export function FaqAccordion() {
  const [open, setOpen] = useState<string[]>(['shipping']);
  return (
    <Accordion
      value={open}
      onChange={setOpen}
      data={[
        { value: 'shipping', label: 'Shipping', content: <Shipping /> },
        { value: 'returns', label: 'Returns & refunds', content: <Returns /> },
      ]}
    />
  );
}

API

Prop

Type

Accessibility

  • Built on Ark UI's Accordion machine and the WAI-ARIA Accordion pattern: each trigger is a real <button> wrapped in a heading (<h2>..<h6> via order, default <h3>), with aria-expanded and aria-controls linking it to its role="region" panel.
  • ↓ / ↑ (or → / ← when orientation="horizontal") move focus between triggers and wrap; Home / End jump to the first/last trigger. Enter / Space toggles the focused trigger, same as any native button.
  • A disabled item's trigger is a real disabled <button> — skipped by arrow-key navigation and unclickable.
  • RTL is honored via the document dir; chevronPosition is logical (start/end), not physical.

Changelog

v0.18.0
  • asChild is a compile error on the data forms of Accordion, Tabs and Breadcrumb, on the ready-made Notification and on AppShell.Sidebar (their Root parts keep it)

    #294
  • Accordion.ItemTrigger and ItemContent compose onto your element under asChild

    #288
v0.8.0
  • New Accordion component

    #120

On this page