Skip to content
cennge-ui
GitHubv0.3.1, changelog

Accordion

Sections under headings that open and close: one at a time, or as many as the reader wants.

Run npm install cennge-ui, then import tailwindcss and cennge-ui/styles.css in your CSS. The stylesheet registers the package with Tailwind itself.

  1. Click a question, or press Enter or Space on it: its answer opens and the open one closes.
  2. The arrow keys move between the questions; Home and End jump to the first and the last.
  3. Click the open question again to close it. With isCollapsible={false} one always stays open.
Live: click it, use the keys.

Field marks

Import
import { Accordion, AccordionItem } from "cennge-ui";
Size
1.2 KB gzip for this import, its dependencies aside
Pattern
WAI-ARIA accordion: each title is a <button> in an <h3>
Keyboard
Enter SpaceArrow up Arrow down HomeEnd
State
Controlled with value and onValueChange, or uncontrolled with defaultValue
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Motion
The panel slides open and closed; with reduced motion it appears at once
Tests
Unit tests

When to use#

An Accordion shortens a long page of sections that are read one at a time: questions and answers, groups of settings, details under a summary. Each heading stays in view, so the reader sees what is there before opening it. When every section matters to everyone, leave them open as plain headings and text.

Similar#

  • Tabs show one panel in place of another, from a row of names; accordion sections open in the flow of the page and can be open together.
  • Card sets content apart but never hides it.

Several open at once#

With selectionMode="multiple" opening one section leaves the others as they are, and value is an array. variant="splitted" turns each item into a card of its own; startContent puts an icon before the title.

Typecheck, unit tests, build, publint and are-the-types-wrong; the unit tests run on React 17, 18 and 19.

multiple.tsx
import { Accordion, AccordionItem } from "cennge-ui";

function WorkflowIcon() {
  return (
    <svg viewBox="0 0 24 24" className="size-5 text-muted" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
      <circle cx="6" cy="6" r="2.5" />
      <circle cx="18" cy="18" r="2.5" />
      <path d="M6 8.5V13a5 5 0 0 0 5 5h4.5" strokeLinecap="round" />
    </svg>
  );
}

export default function Workflows() {
  return (
    <Accordion selectionMode="multiple" variant="splitted" defaultValue={["ci"]} className="max-w-lg">
      <AccordionItem value="ci" title="CI" subtitle="On every push and pull request" startContent={<WorkflowIcon />}>
        Typecheck, unit tests, build, publint and are-the-types-wrong; the unit tests run on React 17, 18 and 19.
      </AccordionItem>
      <AccordionItem value="e2e" title="End-to-end" subtitle="Playwright" startContent={<WorkflowIcon />}>
        Chromium, Firefox and WebKit, on desktop and phone profiles, with axe in both themes.
      </AccordionItem>
      <AccordionItem value="release" title="Release" subtitle="changesets" startContent={<WorkflowIcon />}>
        Versions and the changelog come from changeset files; npm trusted publishing does the rest.
      </AccordionItem>
    </Accordion>
  );
}

Controlled, always one open#

In single mode value is a string. isCollapsible={false} stops the open section from closing, so there is always one; its button then carries aria-disabled, so screen readers know it will not close. A disabled item keeps its place and title, but neither a click nor the keyboard opens it.

Name, description and the default branch of payload-cache.

Open: general

controlled.tsx
import { useState } from "react";
import { Accordion, AccordionItem } from "cennge-ui";

export default function RepositorySettings() {
  const [section, setSection] = useState("general");

  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <Accordion variant="bordered" value={section} onValueChange={setSection} isCollapsible={false}>
        <AccordionItem value="general" title="General">
          Name, description and the default branch of payload-cache.
        </AccordionItem>
        <AccordionItem value="access" title="Access">
          Who can push to main, and who only opens pull requests.
        </AccordionItem>
        <AccordionItem value="danger" title="Danger zone" subtitle="Only the owner can open it" isDisabled>
          Transfer or delete the repository.
        </AccordionItem>
      </Accordion>
      <p className="text-sm text-muted">Open: {section}</p>
    </div>
  );
}

Props#

Accordion#

PropTypeDefault
selectionMode
"multiple" | "single"
"single"
value
string[] | string
–
defaultValue
string[] | string
–
onValueChange
(value: string[]) => void | (value: string) => void
–
variant
"light""bordered""splitted"
"light"
className
string
–
children
React.ReactNode
–
isDisabled
boolean
–
isCollapsible
booleanAllow closing the open item.Only with selectionMode="single"
true

AccordionItem#

PropTypeDefault
valuerequired
string
–
titlerequired
React.ReactNode
–
subtitle
React.ReactNode
–
startContent
React.ReactNode
–
isDisabled
boolean
–

Also takes every attribute of <div>, except title, which mean something else here.