Skip to content

fix(Menu): correct roles on GroupHeading and Separator - #2091

Merged
huntabyte merged 1 commit into
huntabyte:mainfrom
LukianovII:fix/menu-heading-separator-roles
Sep 8, 2026
Merged

huntabyte merged 1 commit into
huntabyte:mainfrom
LukianovII:fix/menu-heading-separator-roles

Conversation

@LukianovII

Copy link
Copy Markdown
Contributor

Fixes #2090.

Menu.GroupHeading and Menu.Separator both render role="group", so a menu that uses them ends up with empty groups in the accessibility tree — a group whose only content is a heading, and another whose only content is a divider.

https://github.com/huntabyte/bits-ui/blob/main/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts#L1630
https://github.com/huntabyte/bits-ui/blob/main/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts#L1658

Both look like copy-paste from MenuGroupState right above them.

The fix follows conventions already in this repo

  • GroupHeading → no role. SelectGroupHeadingState and CommandGroupHeadingState set only an id, and that is enough: the heading names its group through the group's aria-labelledby, which references it by id and does not depend on a role.
  • Separator → role="separator". The top-level SeparatorRootState already resolves decorative ? "none" : "separator", so the menu one was the odd one out.

MenuGroupState, MenuRadioGroupState and MenuCheckboxGroupState keep role="group" — correct for those.

Testing

Added a case to dropdown-menu.browser.test.ts asserting the group is the only group, the separator is a separator, the heading carries no role, and the heading still names its group via aria-labelledby. Reverting either line of the fix turns it red.

pnpm -F tests test:browser --browser chromium:

  • dropdown-menu — 45 passed
  • context-menu + menubar (same menu.svelte.ts) — 49 passed, 1 skipped

pnpm lint clean, prettier --check clean, changeset included (patch).

Note for consumers

Anyone who worked around this by overriding role after spreading props (that was us) can drop the override once this lands — GroupHeading will render no role at all, so a stale role={undefined} override stays harmless either way.

GroupHeading and Separator both rendered role="group", putting empty groups
into the accessibility tree. Drop the role from GroupHeading (matching
Select/Command, whose group headings set no role) and give Separator its own
separator role (matching the top-level Separator component).
@changeset-bot

changeset-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e2a1f14

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
bits-ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor
built with Refined Cloudflare Pages Action

⚡ Cloudflare Pages Deployment

Name Status Preview Last Commit
bits-ui ✅ Ready (View Log) Visit Preview e2a1f14

@LukianovII

Copy link
Copy Markdown
Contributor Author

Test - Chromium is red, but none of the three failures come from this change. Recording the check I did, in case it saves someone the same dig:

failing test evidence it is pre-existing
combobox > should close on outside click same close on outside click failure in #2076, #2084, #2073 (select), #2089, #2088 (combobox + popover) — none of them touch menu.svelte.ts
dropdown-menu > should not scroll to previous dropdown trigger when closing a different dropdown fails identically on #2085, which only renames a Command attribute
context-menu > should allow overriding the pointer events style ran locally on this branch: context-menu 42/42 passed

All three exhaust retry x3 and emit a -4.png screenshot, i.e. they are timing/scroll sensitive rather than assertion failures.

The case this PR adds (dropdown-menu, roles on group/heading/separator) passes on CI — the only dropdown-menu failure in the run is the scroll one above. Happy to rebase if you'd rather see a clean run after the flakes are addressed separately.

@huntabyte
huntabyte merged commit 5627d18 into huntabyte:main Sep 8, 2026
6 of 7 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 8, 2026
dadezzz pushed a commit to dadezzz/events-cash-register that referenced this pull request Sep 12, 2026
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [bits-ui](https://github.com/huntabyte/bits-ui) | [`2.19.0` → `2.19.1`](https://renovatebot.com/diffs/npm/bits-ui/2.19.0/2.19.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/bits-ui/2.19.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/bits-ui/2.19.0/2.19.1?slim=true) |

---

### Release Notes

<details>
<summary>huntabyte/bits-ui (bits-ui)</summary>

### [`v2.19.1`](https://github.com/huntabyte/bits-ui/releases/tag/bits-ui%402.19.1)

[Compare Source](https://github.com/huntabyte/bits-ui/compare/bits-ui@2.19.0...bits-ui@2.19.1)

##### Patch Changes

- fix(Avatar): detach image handlers on destroy and actually apply the load cleanup - [#&#8203;2050](huntabyte/bits-ui#2050) ([#&#8203;2128](huntabyte/bits-ui#2128))

- fix(Menu): `Menu.GroupHeading` no longer sets `role="group"`, `Menu.Separator` now sets `role="separator"` ([#&#8203;2091](huntabyte/bits-ui#2091))

- fix(DismissibleLayer): guard the deferred focus handler against teardown - [#&#8203;2080](huntabyte/bits-ui#2080) ([#&#8203;2126](huntabyte/bits-ui#2126))

- fix(DateField): apply date segments in year/month/day order so a valid day isn't clamped by the placeholder's month in day-first locales ([#&#8203;2123](huntabyte/bits-ui#2123))

- fix(Select): keep the user's scroll position when the scroll down button remounts. The button unmounts at the bottom of the list and remounts as soon as the viewport leaves it, and its mount effect realigned the viewport onto the highlighted item, so a small scroll up from the bottom jumped back to the highlighted item. ([#&#8203;2109](huntabyte/bits-ui#2109))

- fix(DismissibleLayer): outside clicks shortly after a layer opens no longer fail to dismiss it ([#&#8203;2111](huntabyte/bits-ui#2111))

  `DismissibleLayerState` reset its per-interaction state through a 20ms debounce. Because the
  layer's `watch` runs its cleanup once on every open, each layer scheduled a reset 20ms into its
  own lifetime. An outside `pointerdown` landing 10-20ms after that cleanup had its
  "responsible layer" flag cleared by the stale reset before the debounced interact-outside
  handler ran, so the handler bailed and the layer stayed open. The reset is now synchronous.

- fix(ScrollArea): prevent resize work from reading destroyed state after unmount ([#&#8203;2122](huntabyte/bits-ui#2122))

- fix(Combobox): highlight the first matching item after custom filtering updates the rendered items. ([#&#8203;2129](huntabyte/bits-ui#2129))

- fix(Accordion): cancel deferred content work on destroy to avoid `derived_inert` ([#&#8203;2127](huntabyte/bits-ui#2127))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC43OS42IiwidXBkYXRlZEluVmVyIjoiNDQuNzkuNiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

This branch was successfully deployed

1 active deployment
Preview — e2a1f14a Deployed Jul 29, 2026 by github-actions[bot]
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.

Menu.Separator and Menu.GroupHeading expose role="group"

2 participants