Skip to content

feat(core): add the Accordion component - #2

Merged
edwintantawi merged 2 commits into
mainfrom
feat/accordion
Sep 9, 2026
Merged

edwintantawi merged 2 commits into
mainfrom
feat/accordion

Conversation

@edwintantawi

Copy link
Copy Markdown
Member

A stack of disclosure rows in the same token-driven shape as the rest of the set. contained groups the rows on one surface and divides them; separated gives each its own outline. exclusive opens one row at a time, and a row can be disabled.

Underneath it is <details> and <summary>, so the keyboard, the screen reader and find-in-page behaviour are the platform's rather than ours - and exclusive is nothing but the shared name attribute, so no state tracks which row is open.

Four decisions worth writing down:

  • Rows arrive as an items array rather than as children. A compound <Accordion.Item> would have to read the group's variant and its exclusive group name out of React context on every render, and the package README advertises that nothing here does a context lookup in the render path. That paragraph is extended rather than quietly falsified.
  • The chevron mirrors the element's own open state through onToggle instead of turning in CSS. stylex.when.ancestor('[open]') compiles to :where(.marker[open] *), which reaches every chevron under an open row and would rotate the closed ones of a nested Accordion with it.
  • contained draws its dividers by painting the group in the border token and letting a gap of the same width show through between opaque rows. No :first-child rule to exempt the first one, and a theme that thickens its outlines thickens the dividers to match.
  • A disabled row takes the pointer away from its summary and drops it out of the tab order rather than cancelling the click. Enter on a focused summary is a click, so both routes had to go; the not-allowed cursor sits on the row, since a summary that ignores the pointer cannot set one.

The example app gains an Accordion section showing both variants, the exclusive group and the disabled row.

A stack of disclosure rows in the same token-driven shape as the rest of the
set. `contained` groups the rows on one surface and divides them; `separated`
gives each its own outline. `exclusive` opens one row at a time, and a row can
be disabled.

Underneath it is `<details>` and `<summary>`, so the keyboard, the screen reader
and find-in-page behaviour are the platform's rather than ours - and `exclusive`
is nothing but the shared `name` attribute, so no state tracks which row is
open.

Four decisions worth writing down:

- Rows arrive as an `items` array rather than as children. A compound
  `<Accordion.Item>` would have to read the group's variant and its `exclusive`
  group name out of React context on every render, and the package README
  advertises that nothing here does a context lookup in the render path. That
  paragraph is extended rather than quietly falsified.
- The chevron mirrors the element's own open state through `onToggle` instead of
  turning in CSS. `stylex.when.ancestor('[open]')` compiles to
  `:where(.marker[open] *)`, which reaches every chevron under an open row and
  would rotate the closed ones of a nested Accordion with it.
- `contained` draws its dividers by painting the group in the border token and
  letting a `gap` of the same width show through between opaque rows. No
  `:first-child` rule to exempt the first one, and a theme that thickens its
  outlines thickens the dividers to match.
- A disabled row takes the pointer away from its summary and drops it out of the
  tab order rather than cancelling the click. Enter on a focused summary *is* a
  click, so both routes had to go; the `not-allowed` cursor sits on the row,
  since a summary that ignores the pointer cannot set one.

The example app gains an Accordion section showing both variants, the exclusive
group and the disabled row.
`changeset status --since=origin/main` needs a merge base, and
`actions/checkout` clones one commit deep by default: the ref does not exist on
the runner and neither does the history behind it. The step failed on the first
PR to reach it - PR #1 was the release PR, which the `if:` skips - with "Failed
to find where HEAD diverged from origin/main".

release.yml already asks for `fetch-depth: 0` for the same reason. This is the
copy of that line ci.yml should have had.
@edwintantawi
edwintantawi merged commit 7cb629d into main Sep 9, 2026
1 check passed
@edwintantawi
edwintantawi deleted the feat/accordion branch September 9, 2026 18:10
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