Collapse
A collapsible component that supports single panel, accordion, and multiple modes
Installation
Usage
Collapsible Panel
A single collapsible panel.
"use client";
import { Collapse } from "@/ui";
export default function Demo() {
return (
<div>
<Collapse
label="Collapse and Accordion"
className="max-w-md min-w-xs rounded-md border border-border"
>
Use the Collapse component for a single collapsible panel, and the Accordion component for
multiple collapsible panels.
</Collapse>
</div>
);
}
Accordion
Accordion uses mutually exclusive mode by default (at most one panel open); enable multiple to allow several panels to be open at the same time.
Toggle the multiple prop to switch between single-select and multi-select modes. Single-select allows only one panel open at a time, while multi-select supports expanding multiple panels simultaneously.
Use common.classNames to set shared styles for all items. You can customize the root wrapper, trigger button and content panel in one place, instead of configuring className for every single item separately.
The common.icon prop provides a global icon configuration. It accepts static ReactNode or a render function receiving the open state. This avoids repeating icon setup across each item definition.
This component adopts a pure data-driven pattern. All accordion items are configured via the items array. Keeping a single consistent paradigm avoids messy rendering inconsistencies that often occur when mixing data-driven definitions with nested child elements.
"use client";
import { useState } from "react";
import { Accordion } from "@/ui";
import { Plus } from "lucide-react";
const items = [
{
itemKey: "item-1",
label: "Mode",
panel: (
<p className="text-muted-foreground">
Toggle the <code>multiple</code> prop to switch between single-select and multi-select
modes. Single-select allows only one panel open at a time, while multi-select supports
expanding multiple panels simultaneously.
</p>
),
},
{
itemKey: "item-2",
label: "Global Quick Styling",
panel: (
<p className="text-muted-foreground">
Use <code>common.classNames</code> to set shared styles for all items. You can customize the
root wrapper, trigger button and content panel in one place, instead of configuring
className for every single item separately.
</p>
),
},
{
itemKey: "item-3",
label: "Global Custom Icon",
panel: (
<p className="text-muted-foreground">
The <code>common.icon</code> prop provides a global icon configuration. It accepts static
ReactNode or a render function receiving the <code>open</code> state. This avoids repeating
icon setup across each item definition.
</p>
),
},
{
itemKey: "item-4",
label: "Data-driven API",
panel: (
<p className="text-muted-foreground">
This component adopts a pure data-driven pattern. All accordion items are configured via the
items array. Keeping a single consistent paradigm avoids messy rendering inconsistencies
that often occur when mixing data-driven definitions with nested child elements.
</p>
),
},
];
export default function Demo() {
const [value, setValue] = useState<string | undefined>("");
return (
<div>
<Accordion
multiple={false}
activeKeys={value}
onKeyChange={setValue}
className="max-w-md min-w-xs rounded-md border border-border"
items={items}
common={{
icon: (open) => (
<Plus
data-open={open}
className="size-4 transition-transform duration-300 data-[open=true]:-rotate-45"
/>
),
}}
/>
</div>
);
}
Custom
Build an exclusive accordion using the composable components.
CollapseRoot is the container for the accordion/collapse. It lays items out vertically and separates them with not-last:border-b. It accepts a className for custom styling and forwards all native div props.
CollapseTrigger is the interactive button that toggles the panel open or closed. The open prop drives aria-expanded, and hover styles apply while collapsed.
CollapsePanel is the content container that expands and collapses through a grid rows transition driven by the open prop. className applies to the inner content wrapper for custom padding and styling.
"use client";
import { createContext, useContext, useState, useCallback } from "react";
import { CollapsePanel, CollapseRoot, CollapseTrigger } from "@/ui";
import { ChevronDown } from "lucide-react";
type AccordionCtxValue = {
activeKey: string | undefined;
toggle: (key: string) => void;
};
const AccordionDemoCtx = createContext<AccordionCtxValue | null>(null);
const useAccordionDemo = () => {
const ctx = useContext(AccordionDemoCtx);
if (!ctx) throw new Error("must inside AccordionDemoCtx provider");
return ctx;
};
const items = [
{
itemKey: "item-1",
label: "CollapseRoot",
panel: (
<p className="text-muted-foreground">
<code>CollapseRoot</code> is the container for the accordion/collapse. It lays items out
vertically and separates them with <code>not-last:border-b</code>. It accepts a{" "}
<code>className</code> for custom styling and forwards all native div props.
</p>
),
},
{
itemKey: "item-2",
label: "CollapseTrigger",
panel: (
<p className="text-muted-foreground">
<code>CollapseTrigger</code> is the interactive button that toggles the panel open or
closed. The <code>open</code> prop drives <code>aria-expanded</code>, and hover styles apply
while collapsed.
</p>
),
},
{
itemKey: "item-3",
label: "CollapsePanel",
panel: (
<p className="text-muted-foreground">
<code>CollapsePanel</code> is the content container that expands and collapses through a
grid rows transition driven by the <code>open</code> prop. <code>className</code> applies to
the inner content wrapper for custom padding and styling.
</p>
),
},
];
function CollapseItemDemo({ item }: { item: (typeof items)[number] }) {
const { activeKey, toggle } = useAccordionDemo();
const isOpen = activeKey === item.itemKey;
return (
<>
<CollapseTrigger open={isOpen} onClick={() => toggle(item.itemKey)}>
{item.label}
<ChevronDown
data-open={isOpen}
className="size-4 transition-transform duration-300 data-[open=true]:-rotate-180"
aria-hidden
/>
</CollapseTrigger>
<CollapsePanel open={isOpen} className="text-muted-foreground">
{item.panel}
</CollapsePanel>
</>
);
}
export default function Demo() {
const [activeKey, setActiveKey] = useState<string | undefined>("item-1");
const toggle = useCallback((key: string) => {
setActiveKey((prev) => (prev === key ? undefined : key));
}, []);
return (
<AccordionDemoCtx.Provider value={{ activeKey, toggle }}>
<CollapseRoot className="w-md rounded-md border border-border">
{items.map((cfg) => (
<CollapseItemDemo key={cfg.itemKey} item={cfg} />
))}
</CollapseRoot>
</AccordionDemoCtx.Provider>
);
}
API Reference
High-level Components
Ready-to-use composite components.
Collapse
A single collapsible panel with a built-in trigger and animated panel.
| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | ((open: boolean) => React.ReactNode) | - | Content displayed on the header trigger; supports a function receiving the open state |
icon | React.ReactNode | ((open: boolean) => React.ReactNode) | ChevronDown | Expand/collapse icon; supports a function receiving the open state |
open | boolean | - | Controlled open state |
defaultOpen | boolean | false | Initial open state (uncontrolled) |
onOpenChange | (open: boolean) => void | - | Callback when the open state changes |
itemKey | string | - | Suffix for trigger and panel ids, used for accessibility association |
className | ClassNameValue | - | Custom classes, applied to the root |
style | React.CSSProperties | - | Inline styles, applied to the root |
classNames | { trigger?: ClassNameValue; panel?: ClassNameValue } | - | Custom classes for the trigger and panel |
styles | { trigger?: React.CSSProperties; panel?: React.CSSProperties } | - | Inline styles for the trigger and panel |
children | React.ReactNode | - | Panel content |
...props | React.ComponentProps<"div"> | - | All native div props supported |
Accordion
A group of collapsible panels. Mutually exclusive mode by default (at most one panel open at a time); adding the multiple prop switches to multiple mode. The activeKeys type switches with the mode: exclusive is string, multiple is string[].
| Prop | Type | Default | Description |
|---|---|---|---|
items | CollapseItemConfig[] | - | Array of collapsible panel config items. Required |
multiple | boolean | false | Enable multiple mode, allowing several panels to be open at the same time |
defaultActiveKeys | string | string[] | - | Initial open keys (uncontrolled). Exclusive: string; multiple: string[] |
activeKeys | string | string[] | - | Current open keys (controlled). Exclusive: string; multiple: string[] |
onKeyChange | (value: string | undefined) => void | (values: string[]) => void | - | Callback when the open keys change |
common | { className?: ClassNameValue; classNames?: { trigger?: ClassNameValue; panel?: ClassNameValue }; icon?: React.ReactNode | ((open: boolean) => React.ReactNode) } | - | Uniform configuration applied to each item; the item's own className/classNames/icon take precedence |
className | ClassNameValue | - | Container custom styles |
...props | React.ComponentProps<"div"> | - | All native div props supported |
Items are separated by
not-last:border-bby default, which can be overridden viacommon.className.
CollapseItemConfig
Extends all Collapse props except children, open, onOpenChange, and itemKey.
| Field | Type | Default | Description |
|---|---|---|---|
itemKey | string | - | Unique identifier for the item. Required |
panel | React.ReactNode | - | Content displayed in the panel. Required |
label | React.ReactNode | ((open: boolean) => React.ReactNode) | - | Content displayed on the header trigger; supports a function receiving the open state |
icon | React.ReactNode | ((open: boolean) => React.ReactNode) | ChevronDown | Expand/collapse icon; supports a function receiving the open state |
Composable Components
CollapseRoot
Outer container that lays items out vertically and separates them with not-last:border-b.
| Prop | Type | Default | Description |
|---|---|---|---|
className | ClassNameValue | - | Custom styles |
...props | React.ComponentProps<"div"> | - | All native div props supported |
CollapseTrigger
The trigger button; the open prop drives aria-expanded, and hover styles apply while collapsed.
Inside
Collapse, it automatically wiresaria-expanded,aria-controls, and the open toggle; used standalone, passopenand control it yourself viaonClick.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | - | Drives aria-expanded |
className | ClassNameValue | - | Custom styles |
...props | React.ComponentProps<"button"> | - | All native button props supported |
CollapsePanel
DOM: the outer
<section>handles the grid rows collapse animation and accessibility attributes;classNameapplies to the inner content wrapper, which has built-inp-4 pt-0. InsideCollapse, the open state is passed automatically.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | - | Whether expanded, drives the collapse animation |
className | ClassNameValue | - | Custom styles, applied to the inner content wrapper |
...props | React.ComponentProps<"section"> | - | Native section props, for accessibility, data-*, etc. |