react ~8 KB 0 deps v0.3.0 ↗ GitHub ↗

@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 #

PropTypeDefaultDescription
itemsAccordionItem[]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
defaultOpennumber | number[]Index/indices open on first render (uncontrolled)
valuenumber | number[] | nullControlled open state by index
onValueChange(value: number | number[] | null) => voidFires when the open set changes
onOpenChange(index: number, isOpen: boolean) => voidPer-item open/close callback
collapsiblebooleantrueSingle mode: allow re-clicking the open item to close it
animatedbooleantrueEnable smooth height animation
lazybooleanfalseOnly mount panel children after first expand; stays mounted on collapse
disabledbooleanfalseDisable all items
apiRefRef<AccordionImperative>Imperative handle: expandAll, collapseAll, open(i), close(i), toggle(i)
classNamestringExtra class on the root element
styleCSSPropertiesInline style override
Edit this page on GitHub