Accordion
Stacked disclosure panels with token-driven open/close animation and a rotating chevron.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
export default function AccordionDemo() {
return (
<Accordion
type="single"
collapsible
variant="separated"
className="max-w-md"
defaultValue="a"
>
<AccordionItem value="a">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>Yes. It follows the WAI-ARIA design pattern.</AccordionContent>
</AccordionItem>
<AccordionItem value="b">
<AccordionTrigger>Is it themeable?</AccordionTrigger>
<AccordionContent>Yes. Every color comes from ai2 semantic tokens.</AccordionContent>
</AccordionItem>
</Accordion>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/accordionDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install class-variance-authority@^0.7.1 radix-ui@^1.6.1 lucide-react@^1.23.0 tw-animate-css@^1.4.0Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/accordion.tsx"use client"
import * as React from "react"
import { ChevronDown } from "lucide-react"
import { Accordion as AccordionPrimitive } from "@/components/ui/primitives"
import { cn } from "@/lib/utils"
type AccordionVariant = "default" | "separated" | "outline"
const AccordionVariantContext = React.createContext<AccordionVariant>("default")
function Accordion({
className,
variant = "default",
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root> & {
variant?: AccordionVariant
}) {
return (
<AccordionVariantContext.Provider value={variant}>
<AccordionPrimitive.Root
data-slot="accordion"
data-variant={variant}
className={cn(
variant === "separated" && "flex flex-col gap-2",
variant === "outline" && "rounded-lg border border-border",
className
)}
{...props}
/>
</AccordionVariantContext.Provider>
)
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
const variant = React.useContext(AccordionVariantContext)
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn(
variant === "default" && "border-b border-border last:border-b-0",
variant === "outline" && "border-b border-border px-4 last:border-b-0",
variant === "separated" && "rounded-lg border border-border px-4",
className
)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"flex flex-1 items-center justify-between gap-4 py-4 text-left text-sm font-medium outline-none transition-colors duration-(--motion-fast) hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_i]:shrink-0 [&_i]:text-base [&_i]:leading-none [&[data-state=open]>svg[data-accordion-chevron]]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown
data-accordion-chevron=""
className="size-4 shrink-0 text-muted-foreground transition-transform duration-(--motion-base) ease-(--motion-ease)"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="overflow-hidden text-sm text-muted-foreground data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down motion-reduce:animate-none"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Usage
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>Yes. It follows the WAI-ARIA design pattern.</AccordionContent>
</AccordionItem>
</Accordion>Use type="single" collapsible for FAQ-style lists and type="multiple" for settings panels. The height animation runs on the ai2 motion tokens.
Variants
The root variant prop sets the container style and flows to every item through context. default divides rows with a shared border, separated turns each item into its own bordered card, and outline wraps the whole group in one rounded border.
Examples
Multiple
With type="multiple", any number of items can be open and defaultValue takes an array.
Disabled item
Props
The ai2 accordion adds one prop of its own, the root variant. Every other part forwards the corresponding radix Accordion props. The ones you will reach for on the root:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "separated" | "outline" | "default" | Container style. default divides rows with a shared border, separated makes each item a bordered card with a gap, outline wraps the whole group in one rounded border. |
type | "single" | "multiple" | - | Whether one item or several items can be open at once. Required by the radix root. |
collapsible | boolean | false | With type=“single”, allows closing the open item by clicking it again. |
defaultValue | string | string[] | - | Initially open item value (single) or values (multiple) for uncontrolled usage. |
AccordionItem requires a unique value and accepts disabled.
ai2 Accordion: collapsible content for React, styled by tokens
The ai2 Accordion is a shadcn-compatible accordion component for React, built on the radix-ui Accordion primitive and styled with Tailwind CSS v4. It creates vertically stacked, collapsible content panels for FAQs, settings groups, product details and nested navigation, with an animated open and close driven entirely by the shared ai2 motion tokens.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Accordion?
It is a four-part composition: Accordion, AccordionItem, AccordionTrigger and AccordionContent. The anatomy matches shadcn/ui exactly, so existing snippets, muscle memory and AI agents keep working without changes.
The root supports single or multiple expansion, controlled or uncontrolled state, full keyboard navigation, and a variant prop with three container styles: default, separated and outline. Styling comes from the ai2 token file, which means the accordion follows your theme in both light and dark mode from day one.
Why use it
- Accessible by construction: The radix-ui primitive implements the WAI-ARIA accordion pattern: proper roles, aria-expanded state and focus management come for free.
- Keyboard navigation: Arrow keys move between triggers, Home and End jump to the edges, Enter and Space toggle the focused item.
- Motion from tokens: The height animation reads --motion-base and --motion-ease from the shared token file instead of hardcoded durations, so it matches every other ai2 component.
- Single or multiple expansion: type="single" with collapsible fits FAQ lists; type="multiple" fits settings panels where several sections stay open.
- Agent-readable metadata: The registry item describes its parts and intended use in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, its dependencies and the @ai2/tokens theme to your project.
- Rotating chevron indicator: The trigger chevron rotates on open using the same motion tokens as the panel itself.
- Disabled items: Any AccordionItem accepts disabled, which removes it from the tab order and mutes its trigger.
- Controlled and uncontrolled state: Use defaultValue for simple cases or drive value and onValueChange from React state when other UI depends on what is open.
- Data attributes for styling: Every part exposes data-slot and data-state, so you can restyle open and closed states from CSS without forking the component.
- TypeScript source: The file you install is typed end to end and forwards every radix prop, so autocomplete covers the full API.
Production tips
- Label triggers descriptively: Use titles like "Billing" or "Return policy" instead of "Click here". The trigger text is what screen readers and search engines read first.
- Pick the right expansion mode: FAQ pages read best with one item open at a time; dense settings screens usually want type="multiple" so users can compare sections.
- Keep content light: Panels animate their height. Very heavy content inside a panel still renders fine, but consider lazy-loading images inside rarely opened sections.
- Test on mobile: Long trigger labels wrap on small screens. Check that wrapped triggers stay tappable and that the chevron does not collide with the text.
- Respect reduced motion: The open and close animation is CSS-driven; if your product adds custom motion on top, gate it behind prefers-reduced-motion like the rest of ai2 does.
Works with the rest of ai2
The accordion composes naturally with the rest of the registry. Put an ai2 Badge next to a trigger label to show counts or status, drop an ai2 Button inside a panel for actions like Learn more, or use ai2 Field rows to break a long form into collapsible sections.
For page-level composition, pair it with Tabs when the sections are peers rather than a list, or with Card when a group of accordions needs its own surface. Everything shares one token source, so combinations stay visually consistent in both modes.