ComponentsAccordion
Accordion
Let users expand and collapse sections of content.
On this page
Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.
View registry filesImport the component into your page or component file.
import { Accordion } from "@dethink/components";Try the examples, then open the code to use them in your app. Use Tab to reach blades, Enter or Space to toggle, and ArrowUp/ArrowDown/Home/End to move through enabled blades.
Basic
Single mode opens one rounded blade at a time. Clicking the open blade closes it because collapsible defaults to true.
The rounded blade contains both the trigger and the expanded content, so the section reads as one cohesive object.
Put any React content inside the open blade: forms, status lists, controls, or custom product workflows.
Motion layout and presence choreography keeps opening, closing, icon rotation, and sibling reflow coordinated.
Show sourceexamples/accordion/basic.tsx
"use client";
import { Accordion, Button, Link } from "@dethink/components";
function ChevronIcon() {
return (
<svg aria-hidden="true" fill="none" viewBox="0 0 16 16">
<path
d="m5.5 3.5 4 4.5-4 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.75"
/>
</svg>
);
}
const sections = [
{
value: "overview",
title: "Overview",
body: "The rounded blade contains both the trigger and the expanded content, so the section reads as one cohesive object.",
},
{
value: "permissions",
title: "Permissions",
body: "Put any React content inside the open blade: forms, status lists, controls, or custom product workflows.",
},
{
value: "automation",
title: "Automation",
body: "Motion layout and presence choreography keeps opening, closing, icon rotation, and sibling reflow coordinated.",
},
];
export function AccordionBasic() {
return (
<Accordion
aria-label="Workspace settings"
className="max-w-2xl"
defaultValue="overview"
>
{sections.map((section) => (
<Accordion.Item key={section.value} value={section.value}>
<Accordion.Blade>
<Accordion.BladeIcon>
<ChevronIcon />
</Accordion.BladeIcon>
<Accordion.BladeText>{section.title}</Accordion.BladeText>
</Accordion.Blade>
<Accordion.Content>
<div className="grid gap-3">
<p className="text-muted-foreground">{section.body}</p>
<div className="flex flex-wrap gap-2">
<Button size="sm">Open section</Button>
<Button size="sm" variant="outline" asChild>
<Link href="#props">View props</Link>
</Button>
</div>
</div>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion>
);
}Multiple open
Multiple mode keeps opened blades open until each blade is clicked again.
Tokens, radius, dark mode, and density are inherited.
Each open blade stays open until the user clicks it again.
Keyboard, axe, SSR, registry, and Storybook checks cover the component.
Show sourceexamples/accordion/multiple.tsx
"use client";
import { Accordion } from "@dethink/components";
function DotIcon() {
return (
<svg aria-hidden="true" fill="currentColor" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="3" />
</svg>
);
}
const checks = [
["Design", "Tokens, radius, dark mode, and density are inherited."],
["Behavior", "Each open blade stays open until the user clicks it again."],
[
"Quality",
"Keyboard, axe, SSR, registry, and Storybook checks cover the component.",
],
];
export function AccordionMultiple() {
return (
<Accordion
aria-label="Readiness checklist"
className="max-w-2xl"
defaultValue={["design", "behavior"]}
type="multiple"
>
{checks.map(([label, body]) => (
<Accordion.Item key={label} value={label.toLowerCase()}>
<Accordion.Blade>
<Accordion.BladeIcon>
<DotIcon />
</Accordion.BladeIcon>
<Accordion.BladeText>{label}</Accordion.BladeText>
</Accordion.Blade>
<Accordion.Content>
<p className="text-muted-foreground">{body}</p>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion>
);
}Controlled
Drive the open value from external state and use forceMount to preserve content state while closed.
Connect CSV, warehouse, or API sources before mapping fields.
Check warnings, missing owners, and invalid dates before syncing.
Run the final job and keep progress inside the opened blade.
Show sourceexamples/accordion/controlled.tsx
"use client";
import { useState } from "react";
import { Accordion, type AccordionValue } from "@dethink/components";
const items = [
{
value: "import",
label: "Import",
body: "Connect CSV, warehouse, or API sources before mapping fields.",
},
{
value: "review",
label: "Review",
body: "Check warnings, missing owners, and invalid dates before syncing.",
},
{
value: "sync",
label: "Sync",
body: "Run the final job and keep progress inside the opened blade.",
},
];
export function AccordionControlled() {
const [value, setValue] = useState<AccordionValue>("review");
return (
<div className="grid gap-4">
<div className="flex flex-wrap gap-2">
{items.map((item) => (
<button
key={item.value}
className="border-border text-foreground data-[active=true]:bg-muted rounded-md border px-3 py-1 text-sm"
data-active={value === item.value}
onClick={() => setValue(item.value)}
type="button"
>
{item.label}
</button>
))}
<button
className="border-border text-muted-foreground rounded-md border px-3 py-1 text-sm"
onClick={() => setValue(undefined)}
type="button"
>
Close all
</button>
</div>
<Accordion
aria-label="Import workflow"
className="max-w-2xl"
onValueChange={setValue}
value={value}
>
{items.map((item) => (
<Accordion.Item key={item.value} value={item.value}>
<Accordion.Blade>
<Accordion.BladeText>{item.label}</Accordion.BladeText>
</Accordion.Blade>
<Accordion.Content forceMount>
<p className="text-muted-foreground">{item.body}</p>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion>
</div>
);
}Accordion renders a div root. Items render rounded blade containers, blades render native buttons, and content renders labelled regions. Every part accepts className and exposes data-slot, data-state, data-disabled, data-motion-preset, and data-orientation attributes.
| Prop | What it does | Default |
|---|---|---|
type"single" | "multiple" | Controls whether one blade or several blades can be open at the same time. | "single" |
valuestring | undefined | string[] | Controlled open value. Use a string for single mode and string[] for multiple mode. | Not set |
defaultValuestring | string[] | Initial open value for uncontrolled usage. Omit to start fully closed. | Not set |
onValueChange(value: string | undefined | string[]) => void | Fires with the next open value when a blade toggles. | Not set |
collapsibleboolean | In single mode, allows clicking the open blade to close it. Multiple mode always toggles each blade independently. | true |
disabledboolean | Disables every blade in the accordion. | false |
motionPreset"none" | "subtle" | "standard" | "expressive" | Controls Motion layout, content presence, icon rotation, and press feedback. | "standard" |
| Prop | What it does | Default |
|---|---|---|
Item valuestring | Required stable identity for the blade/content pair. Duplicate values throw in development. | Not set |
Item disabledboolean | Disables one blade and removes it from arrow-key navigation. | false |
Blade iconPosition"start" | "end" | Sets icon placement inside the blade trigger. Icon-only blades need an accessible name. | "start" |
Content forceMountboolean | Keeps closed content mounted while hidden, useful for preserving form or widget state. | false |