@mshafiqyajid/react-accordion
Headless accordion hook and styled component for React. Single or multiple open items, smooth CSS grid height animation, controlled/uncontrolled state, variants, lazy mount, imperative API, full keyboard navigation, ARIA-compliant.
Playground #
A headless component provides behaviour and accessibility without any styles, letting you bring your own UI.
Yes — all packages in this collection do nothing at import time and work with Next.js App Router, Remix, and Astro.
The styled component ships a default chevron, but you can override it via renderHeader or build a custom trigger with the headless hook.
Props
TSX
import { AccordionStyled } from "@mshafiqyajid/react-accordion/styled";
import "@mshafiqyajid/react-accordion/styles.css";
<AccordionStyled
items={items}
/>Install #
npm install @mshafiqyajid/react-accordion Quick start #
import { AccordionStyled } from "@mshafiqyajid/react-accordion/styled";
import "@mshafiqyajid/react-accordion/styles.css";
const items = [
{ title: "What is this?", content: "A headless accordion component." },
{ title: "Is it accessible?", content: "Yes — full ARIA and keyboard support." },
];
<AccordionStyled items={items} /> Multiple mode #
By default only one item can be open at a time. Set type="multiple" to allow several open simultaneously.
<AccordionStyled items={items} type="multiple" /> Variants #
Three visual variants: "bordered" (default), "separated", and "flush".
<AccordionStyled items={items} variant="bordered" />
<AccordionStyled items={items} variant="separated" />
<AccordionStyled items={items} variant="flush" /> Default open #
<AccordionStyled items={items} defaultOpen={0} />
<AccordionStyled items={items} type="multiple" defaultOpen={[0, 1]} /> Controlled #
Use value + onValueChange for fully controlled state. In single mode value is number | null; in multiple mode it is number[].
const [open, setOpen] = useState<number | null>(0);
<AccordionStyled
items={items}
value={open}
onValueChange={(v) => setOpen(v as number | null)}
/> Imperative API #
Attach apiRef to get an imperative handle with expandAll, collapseAll, open(index), close(index), and toggle(index).
import { useRef } from "react";
import type { AccordionImperative } from "@mshafiqyajid/react-accordion/styled";
const api = useRef<AccordionImperative>(null);
<AccordionStyled items={items} type="multiple" apiRef={api} />
<button onClick={() => api.current?.expandAll()}>Expand all</button>
<button onClick={() => api.current?.collapseAll()}>Collapse all</button> Per-item options #
Each item in the items array accepts optional keys:
const items = [
{
title: "Normal",
content: "Content",
},
{
title: "Disabled",
content: "Never opens",
disabled: true,
},
{
title: "Always mounted",
content: "Panel stays in DOM even when closed",
forceMount: true,
},
{
title: "Custom header",
content: "Content",
renderHeader: ({ isOpen, toggle }) => (
<span onClick={toggle}>{isOpen ? "▲" : "▼"} Custom</span>
),
},
]; Headless #
import { useAccordion } from "@mshafiqyajid/react-accordion";
const ids = ["item-1", "item-2"];
const { getItemProps } = useAccordion({ items: ids, type: "single" });
{ids.map((id) => {
const { triggerProps, panelProps, isOpen } = getItemProps(id);
return (
<div key={id}>
<button {...triggerProps}>Toggle</button>
{isOpen && <div {...panelProps}>Content</div>}
</div>
);
})} API — AccordionStyled #
| Prop | Type | Default | Description |
|---|---|---|---|
| items | AccordionItem[] | — | Array of { title, content, disabled?, renderHeader?, forceMount? } |
| type | "single" | "multiple" | "single" | Whether multiple items can be open simultaneously |
| variant | "bordered" | "separated" | "flush" | "bordered" | Visual layout variant |
| size | "sm" | "md" | "lg" | "md" | Size variant |
| tone | "neutral" | "primary" | "success" | "danger" | "neutral" | Color tone |
| defaultOpen | number | number[] | — | Index/indices open on first render (uncontrolled) |
| value | number | number[] | null | — | Controlled open state by index |
| onValueChange | (value: number | number[] | null) => void | — | Fires when the open set changes |
| onOpenChange | (index: number, isOpen: boolean) => void | — | Per-item open/close callback |
| collapsible | boolean | true | Single mode: allow re-clicking the open item to close it |
| animated | boolean | true | Enable smooth height animation |
| lazy | boolean | false | Only mount panel children after first expand; stays mounted on collapse |
| disabled | boolean | false | Disable all items |
| apiRef | Ref<AccordionImperative> | — | Imperative handle: expandAll, collapseAll, open(i), close(i), toggle(i) |
| className | string | — | Extra class on the root element |
| style | CSSProperties | — | Inline style override |
Edit this page on GitHub