Skip to main content
BM

Accordion

Headings that open and close a panel of content each, so long content can fold away.

View source on GitHub

AccordionExample.tsx
import { Accordion, AccordionItem } from "@/components/Accordion";

export function AccordionExample() {
  return (
    <Accordion headingLevel={2}>
      <AccordionItem heading="What is this site built with?">
        <p>React, prerendered to static pages, served from S3 and CloudFront.</p>
      </AccordionItem>
      <AccordionItem heading="Can I use these components?">
        <p>They're built for this site, but the source is on GitHub to read and learn from.</p>
      </AccordionItem>
      <AccordionItem heading="How are they tested?">
        <p>Unit tests with axe, and browser tests in Chromium and WebKit.</p>
      </AccordionItem>
    </Accordion>
  );
}

Examples

Independent items

A personal site that doubles as a showcase for its own component library.

Exclusive

Controlled

The item is closed.

Rich heading

Announced as: “Release notes Live, heading level 4, button, collapsed”

Find in page

It's raining cats and ____. Search this page for the missing word: the item opens to show the match.

Regions

Each open panel is announced as a region named by its heading.

Heading look

Customized with component tokens

CSS
.customAccordion {
  --component-accordion-border: var(--color-border-strong);
  --component-accordion-trigger-padding-inline: var(--space-inset-md);
  --component-accordion-panel-padding-inline: var(--space-inset-md);
}

Props

Any native prop not listed here goes to the root element unchanged.

An item's children are its panel. heading must not hold links or buttons: the whole heading is one button. An AccordionItem works alone, with level 3 and the h4 look. With exclusive, items opened together on first render stay open until one opens. Use region only for six or fewer panels.

Accordion props
PropTypeDefaultDescription
headingLevel2 | 3 | 4 | 5 | 63Every item's heading level. Choose it from the page outline.
headingVariantTypographyVariant"h4"Every item's heading look, the same at every level.
exclusivebooleanfalseOpening an item closes the others.
regionbooleanfalseMakes each panel a region named by its heading. Only for six or fewer panels.
AccordionItem props
PropTypeDefaultDescription
headingReactNode—Required. The heading's content, inside its button. No links or buttons.
openboolean—Controlled open state; pair with onOpenChange.
defaultOpenbooleanfalseUncontrolled initial state.
onOpenChange(open: boolean) => void—Called on every requested open or close, controlled or not, including an exclusive group closing it and find-in-page opening it.

Component tokens

Accordion component tokens
TokenCopyDescriptionAliasesLightDarkPreview
--component-accordion-borderLines above each item and below the last (decorative: the button's text and chevron mark each item).color.border.subtle#e5dccbfrom color.sand.200#272f3ffrom color.slate.800
--component-accordion-border-widthLine thickness.effect.border.width2pxfrom effect.line.22pxfrom effect.line.2
--component-accordion-trigger-textHeading button text and chevron.color.text.default#17202bfrom color.gray.900#fffffffrom color.gray.0
--component-accordion-trigger-background-hoverHeading button background on hover (only where the device can hover), as Button's ghost hover.color.surface.sunken#f2ede2from color.sand.100#060a13from color.slate.1000
--component-accordion-trigger-padding-blockBlock padding of the heading button.space.stack.sm0.5remfrom space.20.5remfrom space.2
--component-accordion-trigger-padding-inlineInline padding of the heading button.space.inset.sm0.5remfrom space.20.5remfrom space.2
--component-accordion-trigger-gapSpace between the heading text and the chevron.space.inline.md1remfrom space.41remfrom space.4
--component-accordion-focus-ring-colorFocus indicator, drawn inside the heading button so it stays clear of the lines.color.focus.ring#005f7bfrom color.cyan.700#54d8f4from color.cyan.300
--component-accordion-panel-padding-inlineInline padding of the panel, aligning its content with the heading text.space.inset.sm0.5remfrom space.20.5remfrom space.2
--component-accordion-panel-padding-block-endSpace between the panel's content and the next line.space.stack.md1remfrom space.41remfrom space.4
--component-accordion-transition-durationDuration of the panel's height and the chevron's turn (0ms under reduced motion).motion.duration.base250msfrom motion.time.250Reduced motion: 0ms250msfrom motion.time.250No preview
--component-accordion-transition-easingEasing of the panel's height and the chevron's turn.motion.easing.standardcubic-bezier(0.4, 0, 0.2, 1)from motion.curve.in-outcubic-bezier(0.4, 0, 0.2, 1)from motion.curve.in-outNo preview

Accessibility

  • Each item is a heading holding one button, so the headings stay in the page outline: choose headingLevel to fit it.
  • The button announces whether it's expanded (aria-expanded) and points to its panel (aria-controls).
  • A closed panel is out of the Tab order and the accessibility tree, yet find-in-page and links to its content open it (hidden="until-found", in browsers that support it).
  • No arrow keys move between headings, so arrows keep scrolling the page; Tab reaches every button.
  • With region, each panel is a region named by its heading. More than about six regions clutter the landmarks list, so it's off by default.
  • The height and chevron animate over component.accordion.transition-duration, 0ms under reduced motion, when the item's own button opens or closes it. A change made from outside, by a controlled parent, applies at once.
  • In forced colors, the lines, chevron and focus ring use system colors, and the hover background is dropped.
Accordion keyboard
KeyAction
Tab / Shift+TabMoves between headings' buttons and into open panels.
Enter / SpaceOpens or closes the item.
Arrow keysScroll the page, as usual; there's no movement between headings.