Starter Docs
UI

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 notes, owners and risks in one short handoff before the release window opens.
Launch checklist
Copy, links, analytics, rollback steps and the final approvals, ticked off in one place.
Campaign notes
Notes for each channel, kept next to the task while the list stays calm and folded.
Rollout calendar
Announcements, staging checks, reminders and quiet periods on the same timeline.
Ship the build
The current artifact, its deploy status and the sign-off before the release is done.
Archive assets
Final copy, images and source files move to the campaign folder once rollout ends.

Installation

Add the registry to components.json, once

components.json
{
  "registries": {
    "@rabtx": "http://localhost:3002/r/{name}.json"
  }
}

Add the component

npx shadcn@latest add @rabtx/accordion

Or, without the namespace, by URL:

npx shadcn@latest add http://localhost:3002/r/accordion.json

The 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?
Yes. Upgrades apply at once and are prorated; downgrades start with the next billing period.
Do you offer refunds?
Within 14 days of any charge, no questions asked. Write to billing and we reverse it.
Where is my data stored?
In the EU by default. Team and Scale plans can pin a region per project.

cards

Can I change plans later?
Yes. Upgrades apply at once and are prorated; downgrades start with the next billing period.
Do you offer refunds?
Within 14 days of any charge, no questions asked. Write to billing and we reverse it.
Where is my data stored?
In the EU by default. Team and Scale plans can pin a region per project.

flush

Can I change plans later?
Yes. Upgrades apply at once and are prorated; downgrades start with the next billing period.
Do you offer refunds?
Within 14 days of any charge, no questions asked. Write to billing and we reverse it.
Where is my data stored?
In the EU by default. Team and Scale plans can pin a region per project.

Multiple

type="multiple" lets any number of items open; value and defaultValue are arrays.

Can I change plans later?
Yes. Upgrades apply at once and are prorated; downgrades start with the next billing period.
Do you offer refunds?
Within 14 days of any charge, no questions asked. Write to billing and we reverse it.
Where is my data stored?
In the EU by default. Team and Scale plans can pin a region per project.

Flush

Can I change plans later?
Yes. Upgrades apply at once and are prorated; downgrades start with the next billing period.
Do you offer refunds?
Within 14 days of any charge, no questions asked. Write to billing and we reverse it.
Where is my data stored?
In the EU by default. Team and Scale plans can pin a region per project.

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
Your name, photo and the email people see.
Billing (owners only)
Invoices and the card on file.
Security
Passkeys, two-step codes and active sessions.

Controlled

value and onValueChange control it. "" means none open (single).

Can I change plans later?
Yes. Upgrades apply at once and are prorated; downgrades start with the next billing period.
Do you offer refunds?
Within 14 days of any charge, no questions asked. Write to billing and we reverse it.
Where is my data stored?
In the EU by default. Team and Scale plans can pin a region per project.

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.

PartTokens
Surface (separated, cards)fill --card (3% --foreground on hover), the surface edges, ring, sheen and shade; --border in Flat
Flushdivider --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

EffectCSS
Heightinterpolate-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
Bounceopening 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
Contentrises 4px out of a 4px blur (@starting-style covers the first frame), 50ms after the height starts; closing fades in 120ms
Chevronrotate: 180deg on the swap spring
Fallbackwithout 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 a region named 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.

On this page