Accordion

Displays collapsible content panels that reveal or hide sections of content.

Docs
Content goes here.
import {
  Accordion,
  AccordionItem,
  AccordionItemTrigger,
  AccordionItemContent,
} from "~/components/accordion";

export default function AccordionBasicDemo() {
  return (
    <Accordion defaultValue={["item-1"]}>
      <AccordionItem value="item-1">
        <AccordionItemTrigger>Section Title</AccordionItemTrigger>
        <AccordionItemContent>
          <div class="pb-4 text-sm">Content goes here.</div>
        </AccordionItemContent>
      </AccordionItem>
    </Accordion>
  );
}

Installation

CLI

Run the following command to add the component to your project:

npx @ark-preset/cli@latest add accordion

Manual

Create the recipe file at src/components/recipes/accordion.ts:

import { tv, type VariantProps } from "tailwind-variants";

export const accordionVariants = tv({
  slots: {
    root: "w-full",
    item: "border-b border-border",
    itemTrigger:
      "flex w-full items-center justify-between gap-2 py-4 text-sm font-medium text-start ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
    itemIndicator:
      "size-4 shrink-0 text-foreground transition-transform duration-200 data-[state=open]:rotate-180",
    itemContent:
      "overflow-hidden transition-all duration-200 data-[state=open]:animate-accordion-expand data-[state=closed]:animate-accordion-collapse",
    itemBody: "pb-4 text-sm text-foreground",
  },
});

export type AccordionVariants = VariantProps<typeof accordionVariants>;

Create the component directory and files.

src/components/accordion/accordion.base.tsx:

import { Accordion as ArkAccordion } from "@ark-ui/solid/accordion";
import { splitProps, type Component } from "solid-js";
import { accordionVariants } from "../recipes/accordion";

const styles = accordionVariants();

const Root: Component<ArkAccordion.RootProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkAccordion.Root class={styles.root({ class: local.class })} {...others} />;
};

const RootProvider: Component<ArkAccordion.RootProviderProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkAccordion.RootProvider class={styles.root({ class: local.class })} {...others} />;
};

const Item: Component<ArkAccordion.ItemProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkAccordion.Item class={styles.item({ class: local.class })} {...others} />;
};

const ItemTrigger: Component<ArkAccordion.ItemTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkAccordion.ItemTrigger class={styles.itemTrigger({ class: local.class })} {...others} />
  );
};

const ItemContent: Component<ArkAccordion.ItemContentProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkAccordion.ItemContent class={styles.itemContent({ class: local.class })} {...others} />
  );
};

const ItemIndicator: Component<ArkAccordion.ItemIndicatorProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkAccordion.ItemIndicator class={styles.itemIndicator({ class: local.class })} {...others} />
  );
};

export const Accordion = { Root, RootProvider, Item, ItemTrigger, ItemContent, ItemIndicator };

src/components/accordion/index.tsx:

import { splitProps, type Component } from "solid-js";
import { Accordion as AccordionBase } from "./accordion.base";
import type { Accordion as ArkAccordion } from "@ark-ui/solid/accordion";

const Accordion = AccordionBase.Root;
const AccordionItem = AccordionBase.Item;

const AccordionItemTrigger: Component<ArkAccordion.ItemTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class", "children"]);
  return (
    <AccordionBase.ItemTrigger class={local.class} {...others}>
      {local.children}
      <AccordionBase.ItemIndicator>
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="16"
          height="16"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="m6 9 6 6 6-6" />
        </svg>
      </AccordionBase.ItemIndicator>
    </AccordionBase.ItemTrigger>
  );
};

const AccordionItemContent = AccordionBase.ItemContent;

export { Accordion, AccordionItem, AccordionItemTrigger, AccordionItemContent, AccordionBase };

export { accordionVariants, type AccordionVariants } from "../recipes/accordion";

Note: Make sure your project has the Tailwind CSS theme variables set up (--background, --foreground, --ring, --border, etc.) or override the utility classes to match your design system.

Usage

Basic Usage

Content goes here.
import {
  Accordion,
  AccordionItem,
  AccordionItemTrigger,
  AccordionItemContent,
} from "~/components/accordion";

export default function AccordionBasicDemo() {
  return (
    <Accordion defaultValue={["item-1"]}>
      <AccordionItem value="item-1">
        <AccordionItemTrigger>Section Title</AccordionItemTrigger>
        <AccordionItemContent>
          <div class="pb-4 text-sm">Content goes here.</div>
        </AccordionItemContent>
      </AccordionItem>
    </Accordion>
  );
}

Multiple Items

An accordion with multiple collapsible items.

First content.
import {
  Accordion,
  AccordionItem,
  AccordionItemTrigger,
  AccordionItemContent,
} from "~/components/accordion";

export default function AccordionMultipleItemsDemo() {
  return (
    <Accordion defaultValue={["item-1"]}>
      <AccordionItem value="item-1">
        <AccordionItemTrigger>First Section</AccordionItemTrigger>
        <AccordionItemContent>
          <div class="pb-4 text-sm">First content.</div>
        </AccordionItemContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionItemTrigger>Second Section</AccordionItemTrigger>
        <AccordionItemContent>
          <div class="pb-4 text-sm">Second content.</div>
        </AccordionItemContent>
      </AccordionItem>
    </Accordion>
  );
}

Multiple

Use the multiple prop to allow more than one item to be expanded at the same time.

Yes. Just pass the multiple prop to the Accordion.
Each item tracks its own expanded/collapsed state independently.
import {
  Accordion,
  AccordionItem,
  AccordionItemTrigger,
  AccordionItemContent,
} from "~/components/accordion";

export default function AccordionMultipleDemo() {
  return (
    <div>
      <Accordion multiple defaultValue={["item-1", "item-2"]}>
        <AccordionItem value="item-1">
          <AccordionItemTrigger>Can I open multiple items?</AccordionItemTrigger>
          <AccordionItemContent>
            <div class="pb-4 text-sm text-foreground">
              Yes. Just pass the <code>multiple</code> prop to the Accordion.
            </div>
          </AccordionItemContent>
        </AccordionItem>
        <AccordionItem value="item-2">
          <AccordionItemTrigger>How does it work?</AccordionItemTrigger>
          <AccordionItemContent>
            <div class="pb-4 text-sm text-foreground">
              Each item tracks its own expanded/collapsed state independently.
            </div>
          </AccordionItemContent>
        </AccordionItem>
        <AccordionItem value="item-3">
          <AccordionItemTrigger>Is it accessible?</AccordionItemTrigger>
          <AccordionItemContent>
            <div class="pb-4 text-sm text-foreground">
              Yes. It follows the WAI-ARIA Accordion pattern.
            </div>
          </AccordionItemContent>
        </AccordionItem>
      </Accordion>
    </div>
  );
}

Controlled

Control the expanded state programmatically using value and onValueChange.

import { createSignal } from "solid-js";
import {
  Accordion,
  AccordionItem,
  AccordionItemTrigger,
  AccordionItemContent,
} from "~/components/accordion";

export default function AccordionControlledDemo() {
  const [value, setValue] = createSignal<string[]>([]);

  return (
    <Accordion value={value()} onValueChange={(details) => setValue(details.value)}>
      <AccordionItem value="item-1">
        <AccordionItemTrigger>Controlled Item</AccordionItemTrigger>
        <AccordionItemContent>
          <div class="pb-4 text-sm">Current state: {value().join(", ") || "closed"}</div>
        </AccordionItemContent>
      </AccordionItem>
    </Accordion>
  );
}

Disabled

Disable individual items or the entire accordion.

import {
  Accordion,
  AccordionItem,
  AccordionItemTrigger,
  AccordionItemContent,
} from "~/components/accordion";

export default function AccordionDisabledDemo() {
  return (
    <div>
      <Accordion>
        <AccordionItem value="item-1">
          <AccordionItemTrigger>Active Item</AccordionItemTrigger>
          <AccordionItemContent>
            <div class="pb-4 text-sm text-foreground">This item is interactive.</div>
          </AccordionItemContent>
        </AccordionItem>
        <AccordionItem value="item-2" disabled>
          <AccordionItemTrigger>Disabled Item</AccordionItemTrigger>
          <AccordionItemContent>
            <div class="pb-4 text-sm text-foreground">This item is disabled.</div>
          </AccordionItemContent>
        </AccordionItem>
      </Accordion>
    </div>
  );
}

To disable all items at once, pass disabled to the {"<Accordion>"} root.

Advanced Usage

Root Provider

Use AccordionRootProvider when you need to access the accordion state outside of the accordion tree. This pattern uses the useAccordion hook from Ark UI to create a shared context that both the accordion and external elements can reference.

Value: ["item-1"]
The accordion state is managed externally via useAccordion. The output element above reads accordion().value outside the accordion tree.
import { useAccordion } from "@ark-ui/solid/accordion";
import {
  AccordionBase,
  AccordionItem,
  AccordionItemTrigger,
  AccordionItemContent,
} from "~/components/accordion";

export default function AccordionRootProviderDemo() {
  const accordion = useAccordion({ multiple: true, defaultValue: ["item-1"] });

  return (
    <div class="space-y-4">
      <output class="block text-sm text-muted-foreground">
        Value: {JSON.stringify(accordion().value)}
      </output>

      <AccordionBase.RootProvider value={accordion}>
        <AccordionItem value="item-1">
          <AccordionItemTrigger>What is this demo showing?</AccordionItemTrigger>
          <AccordionItemContent>
            <div class="pb-4 text-sm text-foreground">
              The accordion state is managed externally via <code>useAccordion</code>. The{" "}
              <code>output</code> element above reads <code>accordion().value</code> outside the
              accordion tree.
            </div>
          </AccordionItemContent>
        </AccordionItem>
        <AccordionItem value="item-2">
          <AccordionItemTrigger>Why use RootProvider?</AccordionItemTrigger>
          <AccordionItemContent>
            <div class="pb-4 text-sm text-foreground">
              It gives you access to the accordion context anywhere — not just inside children of
              the root. Useful for external controls, toolbars, or custom UI.
            </div>
          </AccordionItemContent>
        </AccordionItem>
      </AccordionBase.RootProvider>
    </div>
  );
}

The key difference:

  • Accordion (Root) — manages its own state internally. Use for simple, self-contained accordions.
  • AccordionRootProvider — accepts a pre-created accordion context via useAccordion. Use when you need to read or control the accordion state from outside the component tree.

API Reference

See the Ark UI Accordion documentation.