Accordion
A stack of native details elements with one shared state, single or multiple, in separated, cards and flush variants. The open item lifts out of the stack on a bouncy spring and its content fades in out of a blur. Every animation is CSS.
Release brief
Launch checklist
Campaign notes
Rollout calendar
Ship the build
Archive assets
Installation
Add the registry to components.json, once
{
"registries": {
"@rabtx": "http://localhost:3002/r/{name}.json"
}
}Add the component
npx shadcn@latest add @rabtx/accordionOr, without the namespace, by URL:
npx shadcn@latest add http://localhost:3002/r/accordion.jsonThe files land in your components/ui and are styled by your own theme. Add --overwrite to replace a shadcn component with the same name.
The accordion is built on Collapsible, which the command installs with it.
Usage
Installed from the registry, import from @/components/ui/accordion; inside this monorepo, from
@rabtx/ui/accordion. The API is shadcn's.
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@rabtx/ui/accordion";
export function Faq() {
return (
<Accordion type="single" collapsible defaultValue="refunds">
<AccordionItem value="plans">
<AccordionTrigger>Can I change plans later?</AccordionTrigger>
<AccordionContent>Yes. Upgrades apply at once and are prorated.</AccordionContent>
</AccordionItem>
<AccordionItem value="refunds">
<AccordionTrigger>Do you offer refunds?</AccordionTrigger>
<AccordionContent>Within 14 days of any charge, no questions asked.</AccordionContent>
</AccordionItem>
</Accordion>
);
}Every item is a <details> and its trigger the <summary>, so the trigger must be the item's first
child and everything after it is what folds. A single accordion gives its items one name, so the
browser itself keeps one open, even before the page hydrates.
Variants
separated (the default) is one joined stack: the open item lifts out with a gap on either side
and the rows around it round the corners that face it. cards makes every item its own card, and
flush is a plain list divided by lines.
separated
Can I change plans later?
Do you offer refunds?
Where is my data stored?
cards
Can I change plans later?
Do you offer refunds?
Where is my data stored?
flush
Can I change plans later?
Do you offer refunds?
Where is my data stored?
Multiple
type="multiple" lets any number of items open; value and defaultValue are arrays.
Can I change plans later?
Do you offer refunds?
Where is my data stored?
Flush
Can I change plans later?
Do you offer refunds?
Where is my data stored?
Collapsible and disabled
A single accordion keeps its open item open unless it is collapsible (as in Radix and the
WAI-ARIA pattern); that trigger reports aria-disabled while it cannot close. A disabled item is
dimmed, leaves the Tab order and is skipped by the arrow keys.
Profile
Billing (owners only)
Security
Controlled
value and onValueChange control it. "" means none open (single).
Can I change plans later?
Do you offer refunds?
Where is my data stored?
Open: plans
Find in page
Closed content is still in the page. Find in page (Ctrl+F), a link to text inside it
(#:~:text=) or a link to an id inside it opens the item, because the browser opens a <details>
itself (Chromium; other browsers keep it closed). The accordion takes that as a request like a
press: a single accordion moves to the item, and a controlled one sees onValueChange. Native
<details> does this without hidden="until-found".
API
Accordion takes everything a div takes, plus:
Prop
Type
AccordionItem takes everything a details takes except open and name, plus a required
value and disabled. AccordionTrigger takes everything a summary takes, plus icon (a
decorative icon before the title). AccordionContent takes everything a div takes; it is a
region named by its trigger.
Outside React, the class names are exported from @rabtx/ui/accordion/styles
(accordionClass(), accordionParts). It works with no script:
<div class="rx-accordion rx-accordion-separated">
<details class="rx-collapsible rx-accordion-item" name="faq" open>
<summary class="rx-collapsible-trigger rx-accordion-trigger">
<span class="rx-accordion-title">Do you offer refunds?</span>
<span class="rx-collapsible-chevron" aria-hidden="true"></span>
</summary>
<div class="rx-collapsible-content rx-accordion-content">Within 14 days of any charge.</div>
</details>
</div>Theming
The accordion reads only the theme tokens.
| Part | Tokens |
|---|---|
| Surface (separated, cards) | fill --card (3% --foreground on hover), the surface edges, ring, sheen and shade; --border in Flat |
| Flush | divider --border |
| Title, content | --card-foreground, --muted-foreground |
| Focus | --ring |
| Corners | --radius-box (sharp 4, default 12, round 20) |
| Motion | --spring-expand (open, gaps, corners), --spring-glide (close), --spring-swap (chevron), --motion-ease |
How the motion works
| Effect | CSS |
|---|---|
| Height | interpolate-size: allow-keywords lets ::details-content transition block-size from 0 to auto; content-visibility transitions with allow-discrete, so closing stays visible to the end |
| Bounce | opening rides the expand spring (117 / 14.7 / 1, about 5% overshoot, settles in about 890ms), so the height lands a little past its size and settles; closing glides (360 / 32 / 0.6, no overshoot) |
| Lift (separated) | margins and corners picked by [open] + * and :has(+ [open]), transitioned on the same spring while anything is open |
| Content | rises 4px out of a 4px blur (@starting-style covers the first frame), 50ms after the height starts; closing fades in 120ms |
| Chevron | rotate: 180deg on the swap spring |
| Fallback | without interpolate-size (Safari, Firefox today) ::details-content is a one-row grid going from 0fr to 1fr; without ::details-content the content appears at once and still fades |
Accessibility
- Each trigger is a
<summary>: a button with its expanded state, toggled by Enter and Space, reported by the browser. Each panel is aregionnamed by its trigger. - Up and down arrows move between triggers and wrap; Home and End jump to the ends; disabled items are skipped. Tab moves through triggers and the content of open items.
- Closed content is out of the accessibility tree and the Tab order (the browser hides it).
- Keyboard focus shows a 2px ring on the trigger.
- With reduced motion nothing grows, turns or blurs; content fades in 100ms. With
data-motion="off"every state changes at once.
Tabs
WAI-ARIA tabs in segmented, pill, underline and expandable variants. The indicator slides to the selected tab by CSS anchor positioning on a spring, and panels fade in. Every animation is CSS.
Collapsible
A native details element that opens to its own height, fades its content in out of a blur and turns its chevron over. Controlled or not, and opened by find in page. Every animation is CSS.