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.",
),
),
);
}Installation
npx jsrepo add @implementjs/ui/accordion
It installs @implementjs/lucide at the same time.
Copy the file below to src/lib/components/ui/accordion.ts. It imports cn, so copy utils.ts to src/lib/utils.ts too. Then, on top of @implementjs/core and @implementjs/primitives:
npm install @implementjs/lucide
import { Div, type Child, type ComponentProps } from "@implementjs/core";
import { ChevronDownIcon } from "@implementjs/lucide";
import {
Accordion as AccordionPrimitive,
AccordionContent as AccordionContentPrimitive,
AccordionHeader as AccordionHeaderPrimitive,
AccordionItem as AccordionItemPrimitive,
AccordionTrigger as AccordionTriggerPrimitive,
} from "@implementjs/primitives";
import { createComponent } from "@implementjs/primitives";
import { cn } from "@/lib/utils";
export type AccordionProps = ComponentProps<typeof AccordionPrimitive>;
export type AccordionItemProps = ComponentProps<typeof AccordionItemPrimitive>;
export type AccordionHeaderProps = ComponentProps<typeof AccordionHeaderPrimitive>;
export type AccordionTriggerProps = ComponentProps<typeof AccordionTriggerPrimitive>;
export type AccordionContentProps = ComponentProps<typeof AccordionContentPrimitive>;
export const Accordion = createComponent(function Accordion(
{ class: className, ...props }: AccordionProps,
...children: Child[]
) {
return AccordionPrimitive({ "data-slot": "accordion", class: className, ...props }, ...children);
});
export const AccordionItem = createComponent(function AccordionItem(
{ class: className, ...props }: AccordionItemProps,
...children: Child[]
) {
return AccordionItemPrimitive(
{
...props,
"data-slot": "accordion-item",
class: cn("border-b last:border-b-0", className),
},
...children,
);
});
export const AccordionHeader = createComponent(function AccordionHeader(
{ class: className, ...props }: AccordionHeaderProps,
...children: Child[]
) {
return AccordionHeaderPrimitive({ ...props, class: cn("flex", className) }, ...children);
});
export const AccordionTrigger = createComponent(function AccordionTrigger(
{ class: className, ...props }: AccordionTriggerProps,
...children: Child[]
) {
return AccordionHeader(
{ class: "flex" },
AccordionTriggerPrimitive(
{
...props,
"data-slot": "accordion-trigger",
class: cn(
"flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className,
),
},
...children,
ChevronDownIcon({
"aria-hidden": true,
class:
"pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200 ease-in-out",
}),
),
);
});
export const AccordionContent = createComponent(function AccordionContent(
{ class: className, ...props }: AccordionContentProps,
...children: Child[]
) {
return AccordionContentPrimitive(
{
...props,
"data-slot": "accordion-content",
class:
"overflow-hidden text-sm motion-reduce:animate-none data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
},
Div({ class: cn("pt-0 pb-4", className) }, ...children),
);
});Usage
AccordionTrigger is where most of the styling lives. It wraps itself in an AccordionHeader, lays the label out against a chevron, and turns the chevron over while the item is open — so a trigger is just its label.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/lib/components/ui/accordion";
Accordion(
{ type: "multiple" },
AccordionItem(
{ value: "what" },
AccordionTrigger("What is implement?"),
AccordionContent("A signal-based UI framework with no compiler."),
),
);
Animation
The open and close slide comes from the accordion-down / accordion-up keyframes, which the component references but does not define — they belong in your stylesheet, and the introduction has the block to paste. Without them the content still shows and hides correctly; it just snaps.
AccordionContent puts your class on an inner padded Div rather than on the animated element, so padding you add never fights the height animation.
API Reference
Every prop the styling does not consume is forwarded to the Accordion primitive, so the tables below are the whole surface — the behavior props and the styling ones together.
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. Styled with a bottom border, dropped on the last item. 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. The styled trigger wraps itself in an AccordionHeader and appends a chevron that turns over while the item is open. 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. class lands on an inner padded Div, so the animated element keeps the overflow and height classes it needs. The slide comes from the accordion keyframes in your stylesheet. 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" |