Accordion
A vertically stacked set of headings that each reveal a section of content. Use it to shorten a long page, never to hide information a reader needs to complete the task in front of them.
Live preview
Rendered by the component itself from the same generated tokens as the rest of the site.
<Accordion type="single" collapsible defaultValue="ready">
<AccordionItem value="ready" title="Is Graphite UI production ready?">
Thirty-six components carry a versioned contract and are checked against the Figma kit on every build.
</AccordionItem>
<AccordionItem value="carbon" title="Does it require Carbon?">
Not for the components. The site still leans on Carbon for a few pieces of chrome, and moving off it is a tracked migration.
</AccordionItem>
<AccordionItem value="theming" title="How does theming work?">
One source color becomes the ramps and the roles. Change it in the header and the page repaints.
</AccordionItem>
</Accordion>Installation
Install it with the shadcn CLI, along with everything it uses. New project? Set it up first, as Quick start shows.
npx shadcn@latest add https://www.graphite-ui.com/r/accordion.jsonimport {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from '@/components/ui/accordion'Anatomy
Slots come from the contract, so this list and the code cannot disagree about what the component is made of.
- TriggerRequiredThe heading. It is a button, and it owns the expanded state.
- IndicatorRequiredRotates to show state. Never the only signal that a panel is open.
- PanelRequiredThe revealed region, labelled by its own trigger.
- Panel controlOptionalOne instance-swap slot inside the panel, for a control the content needs.
Variants
Three axes travel from the kit into the code as props. Size, alignment and flush are all one-word choices; everything else about an accordion is composition.
States
These are variant axes in the kit and pseudo-classes in the code. A State=Hover variant is a picture of a behaviour, not an instruction to add a hover prop, so the page shows them as a matrix rather than as props.
API reference
Generated from the contract, versioned with it, and checked by drift-check. This table cannot describe props the component does not have.
| Prop | Type | Default | Notes |
|---|---|---|---|
| type | 'single' | 'multiple' | 'single' | Whether one panel or several may be open at once. |
| collapsible | boolean | false | Lets the open panel close again, leaving none open. Only meaningful for single; a multiple accordion can always close every panel. |
| size | 'sm' | 'md' | 'lg' | 'md' | The kit's Small, Medium and Large. |
| flush | boolean | false | Drops the outer rules and the horizontal inset, so the list sits flush inside a container that already has its own edge. |
| align | 'left' | 'right' | 'right' | Which edge the indicator sits on. |
| className | string | — | Merged after the variant recipe, so a caller can extend without forking. |
Design tokens
Every swatch is live. Change the source color in the header and this table repaints, because it reads the same roles the component does.
on-surfaceTrigger label, panel copy and the indicator glyph, all Text/text-primary and Icon/icon-primary in the kit.outlineThe rule between items and the outer rule when flush is false, through its subtle step (the kit's Border/border-subtle-00).elevationThe trigger's hover fill, elevation-02 (the kit's Layer/layer-hover-01). The item itself is transparent.primaryThe focus ring on the trigger, through the page-level focus variable, and the disabled title, copy and indicator, through its disabled-content step.spacingTrigger and panel padding.textTrigger label and panel copy, both Body/3 at every size; size changes the trigger height only.motionThe panel's open and close transition, on the settle curve.
Usage
The contract's prohibitions, written as the choices you will actually face.
Do
- Let a reader open more than one panel when the panels are independent.
- Keep the trigger a real button, so Enter and Space both work.
- Use flush when the list already sits inside something with a border.
- Pair the indicator with a change the reader can also feel in layout.
Don’t
- Hide anything the reader needs to finish the task in front of them.
- Nest an accordion inside an accordion. Two levels of disclosure is a navigation problem wearing a component.
- Animate the panel on a hardcoded duration. The motion tokens exist, so bind them.
- Let the indicator carry the open state on its own.
Accessibility
What the component does for you, and what it leaves to you.
- Keyboard
- Tab moves between triggers. Enter and Space both toggle the panel the focus is on. Nothing traps focus inside a panel.
- Roles
- Each trigger is a button carrying aria-expanded and aria-controls; each panel is labelled by its own trigger.
- Focus
- The focus ring is
--graphite-focusand is never removed, only moved. - Contrast
- Trigger label against surface is measured at the theme’s target, AA or AAA, and the pairing is checked rather than reviewed.
- Motion
- The open transition respects prefers-reduced-motion and falls back to an instant change.
Figma parity
The kit's Accordion page ships 141 variants across 5 sets, and 120 of them are the one Accordion item set. The code exposes 6 props. This table is where those two facts are reconciled instead of quietly diverging.
| Kit axis | Values | Code | How it maps |
|---|---|---|---|
| Size | Small · Medium · Large | size | One to one. |
| Alignment | Right · Left | align | One to one. |
| Flush | False · True | flush | One to one. |
| Expanded | False · True | — | Runtime state, not a prop. The kit draws it because Figma has no other way to show it. |
| State | Enabled → Skeleton | — | Pseudo-classes in code. Governance rule 7: a State=Hover variant is not an instruction to add a hover prop. Hover fills elevation-02, the kit’s layer-hover-01. Disabled dims the title, copy and chevron; the kit leaves the chevron at full strength, which the code treats as a slip. Skeleton has no counterpart: nothing in an accordion loads asynchronously. |
| Slot | Boolean + swap | children | Composition. The caller passes content instead of choosing from a fixed pair. |
