Skip to content
Dethink Components

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 files

Import the component into your page or component file.

Usage
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.

View props
Show sourceexamples/accordion/basic.tsx
examples/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.

Show sourceexamples/accordion/multiple.tsx
examples/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.

Check warnings, missing owners, and invalid dates before syncing.

Show sourceexamples/accordion/controlled.tsx
examples/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.

Accordion props
PropWhat it doesDefault
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[]) => voidFires with the next open value when a blade toggles.Not set
collapsiblebooleanIn single mode, allows clicking the open blade to close it. Multiple mode always toggles each blade independently.true
disabledbooleanDisables every blade in the accordion.false
motionPreset"none" | "subtle" | "standard" | "expressive"Controls Motion layout, content presence, icon rotation, and press feedback."standard"
Part props
PropWhat it doesDefault
Item valuestringRequired stable identity for the blade/content pair. Duplicate values throw in development.Not set
Item disabledbooleanDisables 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 forceMountbooleanKeeps closed content mounted while hidden, useful for preserving form or widget state.false