Skip to content

feat(react): add the Accordion primitive#18

Open
ivanbanov wants to merge 2 commits into
mainfrom
feat/accordion
Open

feat(react): add the Accordion primitive#18
ivanbanov wants to merge 2 commits into
mainfrom
feat/accordion

Conversation

@ivanbanov

Copy link
Copy Markdown
Member

Summary

Adds the Accordion primitive as two packages:

  • @dunky.dev/accordion — the framework-free core: a state machine (built on @dunky.dev/state-machine) owning the open set, toggle gating, and keyboard-focus movement.
  • @dunky.dev/react-accordion — the thin React binding that renders the compound parts and carries the machine's decisions to the DOM.

An accordion is a stacked set of headers, each expanding or collapsing a section beneath it, per the WAI-ARIA APG pattern. The type discriminant picks the value shape (single speaks string | null, multiple speaks string[]), so two open items in single mode is unrepresentable. A controlled value is authoritative: a press reports the value it asked for through onValueChange and the UI follows the prop.

Anatomy

<Accordion>          - root; owns the open value and keyboard focus, renders no element of its own
  |_ <Item>          - one disclosure: identified by its value, disableable
     |_ <Header>     - the heading wrapper that gives the trigger its outline level
     |  |_ <Trigger> - the button that toggles the item and hosts keyboard navigation
     |_ <Content>    - the section the trigger expands and collapses; hidden while closed

Behavior

  • Enter / Space toggles the item (native button activation). Single mode is non-collapsible by default per APG; collapsible opts into closing the open item. Multiple mode toggles freely.
  • Arrow keys move focus across enabled triggers with wrap, axis following orientation (vertical: ArrowDown/ArrowUp; horizontal: ArrowRight/ArrowLeft). Home / End jump to the first / last enabled trigger. Disabled items are skipped; a disabled accordion leaves the keys to the page.
  • ARIA: trigger carries aria-expanded + aria-controls; content is a role="region" labelled by its trigger; ids derive from the root id plus the URI-encoded item value so cross-references always resolve. Disabled triggers — and the open trigger in single non-collapsible mode, per APG — expose aria-disabled while staying focusable.
  • Styling hooks: every part carries data-state (open / closed), data-disabled, and data-orientation.
  • Toggle gating and focus movement live in the core machine; the React layer adds no behavior of its own.

Test plan

  • pnpm test:ci — 133 tests, 9 files, all passing
  • pnpm typecheck
  • pnpm lint
  • pnpm format:check
  • pnpm build

🤖 Generated with Claude Code

ivanbanov and others added 2 commits July 18, 2026 20:48
Adds @dunky.dev/accordion (framework-free state machine) and
@dunky.dev/react-accordion (React binding). Anatomy: Accordion root
(renders no element) > Item > Header > Trigger, plus Content. Stacked
disclosure sections per the WAI-ARIA APG pattern: single (optionally
collapsible) or multiple open items, orientation-aware arrow/Home/End
trigger navigation, and an authoritative controlled value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ivanbanov ivanbanov changed the title feat: add the Accordion primitive feat(react): add the Accordion primitive Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant