Accordion
Accordion shows and hides sections of content under clickable headers. Use it for study-guide sections, FAQs and groups of settings.
Beta until Figma has an Accordion component (C3). The card style follows the live nuton.app FAQ; the flush style, chevron, icon and description are code-only. Everything on this page is implemented and tested.
Preview
Change the controls to try every supported combination. The preview renders the real component and the code updates with it.
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@nuton/ui'
<Accordion defaultValue="sources">
<AccordionItem value="sources">
<AccordionTrigger>What can I turn into a course?</AccordionTrigger>
<AccordionContent>A topic you type, or your own PDFs, slide decks, YouTube videos, audio, web pages and notes.</AccordionContent>
</AccordionItem>
<AccordionItem value="privacy">
<AccordionTrigger>Who can see my study materials?</AccordionTrigger>
<AccordionContent>Only you, unless you share a space. Your uploads stay in your own spaces.</AccordionContent>
</AccordionItem>
<AccordionItem value="revision">
<AccordionTrigger>How does revision work?</AccordionTrigger>
<AccordionContent>Each lesson ends with flashcards and a short quiz. Nuton brings back the cards you found hardest.</AccordionContent>
</AccordionItem>
</Accordion>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages. They aren't published to a registry yet (D4: registry deferred; D12: distribution waits on the provenance review). Inside the Nuton monorepo, add them as workspace dependencies. The commands below apply once the packages are published.
1. Add the packages
pnpm add @nuton/ui @nuton/tokens @remixicon/reactnpm install @nuton/ui @nuton/tokens @remixicon/reactyarn add @nuton/ui @nuton/tokens @remixicon/reactPeer dependencies: react and react-dom 18 or later. The chevron indicator uses @remixicon/react. Accordion is built in-house: it doesn't need @radix-ui/react-accordion.
2. Load tokens and component styles once
// app/layout.tsx (or your app's entry file)
import '@nuton/tokens/tokens.css' // --nu-* design tokens
import '@nuton/ui/styles.css' // component styles (uses the tokens)Closed panels are hidden by the component styles, so the stylesheet is required. Headers and panels use --nu-font-body (Inter), as described on the Button page.
Usage
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@nuton/ui'
export function CourseFaq() {
return (
<Accordion defaultValue="sources">
<AccordionItem value="sources">
<AccordionTrigger>What can I turn into a course?</AccordionTrigger>
<AccordionContent>A topic you type, or your own PDFs, slides, videos and notes.</AccordionContent>
</AccordionItem>
<AccordionItem value="privacy">
<AccordionTrigger>Who can see my study materials?</AccordionTrigger>
<AccordionContent>Only you, unless you share a space.</AccordionContent>
</AccordionItem>
</Accordion>
)
}- Each
AccordionItemneeds a uniquevalue. PassdefaultValueto start with an item open, orvalue+onValueChangeto control it. type="single"(default) keeps one item open, like the live FAQ. Usetype="multiple"when people compare sections, such as a study guide.- Set
headingLevelto fit the page outline. The default is 3, so an accordion under anh2needs nothing. - Keep headers short and specific: they are the button's accessible name.
- Don't hide content people must read to finish a task. Accordion is for optional or scannable detail.
Variants and sizes
Two appearances and two indicators. Each header can add an icon and a description.
Appearance
<Accordion defaultValue="privacy"> // appearance="card" (default)
<AccordionItem value="sources">…</AccordionItem>
<AccordionItem value="privacy">…</AccordionItem>
</Accordion><Accordion appearance="flush" defaultValue="revision">
<AccordionItem value="sources">…</AccordionItem>
<AccordionItem value="privacy">…</AccordionItem>
</Accordion>card is the live nuton.app FAQ: separate bordered cards, 12px apart, filled with Weak-50 when open. flush(code-only) splits rows with lines and has no side padding, for settings pages and panels that already have a frame.
Indicator
<Accordion indicator="chevron">…</Accordion> // indicator="plus" is the defaultThe plus turns into a minus as it rotates (live FAQ). The chevron (code-only) turns over. Both are decorative: aria-expanded carries the state.
Icon and description
<AccordionItem value="ch-1">
<AccordionTrigger icon={<RiBookOpenLine />} description="3 lessons · 14 min">
Introduction to design principles
</AccordionTrigger>
<AccordionContent>…</AccordionContent>
</AccordionItem>The icon is hidden from screen readers. The description is part of the button's name, read after the title, so the name matches the visible text (WCAG 2.5.3); the panel region is named by the title alone. The panel text lines up with the title.
Dimensions
| Part | Size | Spacing | Type |
|---|---|---|---|
| Card header | 56px tall for one line (1 + 13 + 28 + 13 + 1) | Padding 14 / 20 (inside a 1px border); 10px between icon, text and indicator | Title Label/Medium 16/28 Strong-950 |
| Card panel | Content height | 10px under the title; 14px bottom; aligned with the title | Paragraph/Small 14/24 Sub-600 |
| Card stack | Full width | 12px between cards; radius 16 (live) | — |
| Flush row | 56px plus a 1px line | Padding 14 / 0 | As card |
| Icon / indicator | 20px / 24px box (plus glyph 12 × 2; chevron 20) | Centred on the 28px title line | — |
| Description | — | Under the title | Paragraph/X Small 12/16 Sub-600 |
Taken from the component CSS. Radius 16 matches the live FAQ (owner decision, 2026-10-14).
States and interactions
Each header is a button, so it has the usual button states. The open state animates the panel's height.
aria-expanded="false". The panel is hidden and inert.disabled: skipped by Tab and arrow keys; text Disabled-500.collapsible={false}: the open header is aria-disabled until another item opens.1 of 3 sections open
value.The height, fill and indicator animate over --nu-duration-slow (300ms). Under reduced motion they change at once.
Tokens
| Token | Default | Used for |
|---|---|---|
--nu-accordion-border | Stroke/Soft-200 | Card border, flush lines |
--nu-accordion-bg-hover · --nu-accordion-bg-open | Bg/Weak-50 | Card hover and open fill |
--nu-accordion-fg · --nu-accordion-fg-sub | Text/Strong-950 · Text/Sub-600 | Title · panel and description |
--nu-accordion-marker · --nu-accordion-icon | Icon/Strong-950 · Icon/Sub-600 | Plus · icon and chevron |
--nu-accordion-focus · --nu-accordion-focus-shadow | State/Primary/Base · buttons/primary-focus | Card focus |
--nu-accordion-radius · --nu-accordion-gap | 12px · 12px | Card radius · space between cards |
--nu-accordion-duration | --nu-duration-slow | All transitions |
Composition examples
FAQ on a help page
One question open at a time, as on nuton.app. The page heading is an h2, so the questions stay h3.
Groups of settings
Flush rows with icons and a summary line. Several groups can be open; controls inside work as usual.
Study guide sections
Use type="multiple" so students can keep several chapters open. Pair it with an Expand all button when there are many sections (see Controlled).
API reference
Accordion props
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'single' | 'multiple' | 'single' | single: one item open at a time. multiple: any number. |
value | string | null (single) · string[] (multiple) | — | Open item(s), controlled. Use with onValueChange. |
defaultValue | string | null (single) · string[] (multiple) | — | Open item(s) on first render, uncontrolled. |
onValueChange | (value: string | null) => void · (value: string[]) => void | — | Called when an item opens or closes. Single gets null when the open item closes. |
collapsible | boolean | true | Single only. false keeps one item open: the open header becomes aria-disabled. |
appearance | 'card' | 'flush' | 'card' | card: bordered cards (live FAQ). flush: rows split by lines (code-only). |
indicator | 'plus' | 'chevron' | 'plus' | Marker at the end of each header. |
headingLevel | 2 | 3 | 4 | 5 | 6 | 3 | Heading level that wraps every header button. |
disabled | boolean | false | Disables every item. |
ref | Ref<HTMLDivElement> | — | Forwarded to the root. |
…rest | HTMLAttributes<HTMLDivElement> | — | Passed to the root (className, id, aria-*…). |
AccordionItem props
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | string | — | Unique key for this item. |
disabled | boolean | false | Disables this item’s header. |
ref | Ref<HTMLDivElement> | — | Forwarded to the item. |
…rest | HTMLAttributes<HTMLDivElement> | — | Passed to the item. |
AccordionTrigger props
| Prop | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | — | Header text and the button’s accessible name. |
description | ReactNode | — | Line under the title. Read after the title as part of the button's name. |
icon | ReactNode | — | Decorative 20px icon before the title. |
headingLevel | 2 | 3 | 4 | 5 | 6 | — | Overrides the Accordion's headingLevel for this header. |
onClick | MouseEventHandler | — | Runs first; call event.preventDefault() to stop the toggle. |
ref | Ref<HTMLButtonElement> | — | Forwarded to the button. |
…rest | HTMLAttributes<HTMLButtonElement> | — | Passed to the button. |
AccordionContent props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Panel content. It stays mounted when closed. |
ref | Ref<HTMLDivElement> | — | Forwarded to the region. |
…rest | HTMLAttributes<HTMLDivElement> | — | Passed to the region. |
Styling hooks
The root is .nu-accordion with data-appearance, data-indicator and data-type. Items, buttons and panels carry data-state="open|closed". Use the tokens in Tokens to adjust it.
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves to the next header, then into an open panel’s content. |
| Enter / Space | Opens or closes the focused header’s panel. |
| ↓ / ↑ | Moves to the next or previous header, wrapping round. Disabled headers are skipped. |
| Home / End | Moves to the first or last header. |
Screen readers
WAI-ARIA accordion: each header is a button inside a heading (headingLevel), with aria-expanded and aria-controls. Each panel is a region named by its header. Closed panels are inert and visibility: hidden, so they are not read or focusable. The indicator and icon are hidden.
Focus
Focus stays on the header when it toggles. Card: primary border plus the focus halo round the whole card; flush: a 2px ring. Forced-colours mode uses Highlight.
Contrast and colour
Titles are Strong-950 (16.7:1 on Weak-50); panel text and descriptions Sub-600 (6.24:1 on Weak-50, 6.69:1 on white). The card border (Soft-200, 1.19:1) is decorative: the header text identifies each item. State is never shown by colour alone.
Touch and pointer
The whole header is the target: at least 56px tall and full width, above the 24 × 24 minimum.
You must: set headingLevel to fit the page outline; give every item a unique value; keep header text meaningful on its own (“Billing”, not “More”).
Figma mapping
Figma has no Accordion (C3; every page searched on 2026-10-14). The card follows the live nuton.app FAQ. When a Figma component is added, map its properties here and re-check the code-only choices.
Guidelines and related components
Use Accordion for
- FAQs and help content people scan for one answer.
- Long study guides where students open one chapter at a time.
- Groups of settings that would make a page too long.
Don't use Accordion for
- Switching between views of the same thing. Use Tabs.
- Content everyone must read, such as a lesson or instructions.
- A single “Show more”. Use a button that reveals the rest.
Do and don't
Common mistakes
- Reusing a
valuein two items: both open and close together. - Leaving
headingLevelat 3 under anh3, which breaks the page outline. - Loading the component without
@nuton/ui/styles.css: closed panels then show. - Putting the only copy of required instructions in a closed panel.