Accordion
Expand and collapse sections of content, one at a time or several at once.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/lib/components/ui/accordion";
export default function AccordionDemo() {
return Accordion(
{ type: "multiple", class: "w-full max-w-md" },
AccordionItem(
{ value: "what" },
AccordionTrigger("What is implement?"),
AccordionContent("A signal-based UI framework with no compiler."),
),
AccordionItem(
{ value: "why" },
AccordionTrigger("Why no compiler?"),
AccordionContent("Your app is plain TypeScript that builds real DOM nodes."),
),
AccordionItem(
{ value: "styling" },
AccordionTrigger("How do I style it?"),
AccordionContent(
"Every part exposes data attributes like data-state, so plain CSS or Tailwind works.",
),
),
);
}An accordion is a stack of items that open and close. Accordion is the root, AccordionItem is one section, AccordionTrigger is the control that toggles it, and AccordionContent is the body.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@implementjs/primitives";
Accordion(
AccordionItem(
{ value: "what" },
AccordionTrigger("What is implement?"),
AccordionContent("A signal-based UI framework with no compiler."),
),
AccordionItem(
{ value: "why" },
AccordionTrigger("Why no compiler?"),
AccordionContent("Your app is plain TypeScript that builds real DOM nodes."),
),
);
Each part accepts optional props and children — pass a props object when you need attributes, or pass children directly. See createComponent. Extra props are forwarded onto the underlying Div or Button.
Single or multiple
type defaults to "single": opening one item closes the others. Pass "multiple" to let several items stay open at once.
Accordion(
{ type: "multiple" },
AccordionItem({ value: "a" }, AccordionTrigger("A"), AccordionContent("First")),
AccordionItem({ value: "b" }, AccordionTrigger("B"), AccordionContent("Second")),
);
Every item needs a stable value. That string is what the root tracks as its open state, so it also has to be unique within the accordion.
Controlling the open items
Pass a signal as value to own the open state from outside. A "single" accordion takes a Signal<string | null>, a "multiple" accordion a Signal<string[]>:
import { signal } from "@implementjs/core";
const open = signal<string | null>("what");
Accordion(
{ value: open },
AccordionItem({ value: "what" }, AccordionTrigger("A"), AccordionContent("First")),
AccordionItem({ value: "why" }, AccordionTrigger("B"), AccordionContent("Second")),
);
open.set("why"); // opens "why", closing "what"
The trigger and the content
AccordionTrigger renders a Button. Clicking it toggles the item. AccordionContent is a Div that sets the hidden attribute while the item is closed, so the body is out of the accessibility tree and not shown.
If you want find-in-page to still search closed sections, pass hiddenUntilFound. Closed content then uses hidden="until-found" instead of the boolean hidden attribute, and the browser can reveal a match.
AccordionContent({ hiddenUntilFound: true }, LongAnswer());
Headings
Wrap the trigger in AccordionHeader when the item title should be a heading. It renders a Div with role="heading" and aria-level (3 by default):
AccordionItem(
{ value: "what" },
AccordionHeader({ level: 3 }, AccordionTrigger("What is implement?")),
AccordionContent("..."),
);
Styling
Every part sets a data-accordion-* attribute so you can target it in CSS, and items, triggers, headers, and content expose data-state as "open" or "closed":
AccordionTrigger(
{ class: "flex w-full items-center justify-between py-2 font-medium" },
"What is implement?",
);
AccordionContent(
{ class: "pb-4 text-sm text-foreground/70" },
"A signal-based UI framework with no compiler.",
);
The hidden attribute already hides closed content. data-state is there for transitions, chevrons, and anything else that should react to open versus closed without you threading a signal through.
Animating open and close
AccordionContent measures itself whenever it opens or closes and exposes the result as --ip-accordion-content-height and --ip-accordion-content-width on the element, so keyframes can animate between zero and the natural size. When an item closes, the content keeps rendering until any animation running on it finishes — only then does the hidden attribute go on.
[data-accordion-content] {
overflow: hidden;
}
[data-accordion-content][data-state="open"] {
animation: accordion-down 0.2s ease-out;
}
[data-accordion-content][data-state="closed"] {
animation: accordion-up 0.2s ease-out;
}
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--ip-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--ip-accordion-content-height);
}
to {
height: 0;
}
}
Content that is open on first render does not replay its open animation on page load.
API Reference
Accordion
The root. Owns which items are open and the arrow-key focus movement. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
type | "single" | "multiple" | "single" | Whether opening an item closes the others, or several can stay open. |
value | Signal<string | null> (single) | Signal<string[]> (multiple) | — | The open item(s). Pass a signal to control the accordion from outside; omit it for uncontrolled state. |
disabled | Signal<boolean> | boolean | false | Disables every item in the accordion. |
loop | boolean | true | Whether arrow keys wrap from the last trigger back to the first. |
orientation | "horizontal" | "vertical" | "vertical" | Which arrow keys move focus between triggers: vertical is Up/Down, horizontal is Left/Right. |
| Data attribute | Value |
|---|---|
[data-accordion-root] | Present |
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled |
AccordionItem
One section of the accordion. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value* | string | — | Identifies the item. Must be unique within the accordion. |
disabled | Signal<boolean> | boolean | false | Disables this item. |
| Data attribute | Value |
|---|---|
[data-accordion-item] | Present |
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
[data-orientation] | "horizontal" | "vertical" |
AccordionTrigger
Toggles its item open and closed. Renders a Button; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | Signal<boolean> | boolean | false | Disables this trigger. |
| Data attribute | Value |
|---|---|
[data-accordion-trigger] | Present |
[data-state] | "open" | "closed" |
[data-value] | The item's value |
[data-disabled] | Present when disabled |
[data-orientation] | "horizontal" | "vertical" |
AccordionContent
The body of an item. Hidden with the `hidden` attribute while closed; a close animation on it finishes before the attribute is set. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
hiddenUntilFound | boolean | false | Closed content uses hidden="until-found" so find-in-page can reveal it. |
| Data attribute | Value |
|---|---|
[data-accordion-content] | Present |
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
[data-orientation] | "horizontal" | "vertical" |
| CSS variable | Description |
|---|---|
--ip-accordion-content-height | The natural height of the content, for open/close animations. |
--ip-accordion-content-width | The natural width of the content, for open/close animations. |
AccordionHeader
Wraps a trigger in role="heading" when the item title should be a heading. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
level | 1 | 2 | 3 | 4 | 5 | 6 | 3 | The aria-level of the heading. |
| Data attribute | Value |
|---|---|
[data-accordion-header] | Present |
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
[data-orientation] | "horizontal" | "vertical" |
[data-heading-level] | "1" – "6" |