#7537·ariakit

Treat an empty aria-current value as not current in ui-selected

Author: diegohazCreated Sep 16, 2026Updated Sep 16, 2026
Labelsexperimental buga11yp2easyui

Context

Found while reviewing #7536, which moves navLink and navGlider off the ui-nav-current custom variant and onto the shared ui-selected variant. Reviewing that swap surfaced a defect in ui-selected itself rather than in the pull request.

Problem

ui-selected and ui-selected-within in packages/ariakit-ui/src/styles/ui.css treat the current-item state as [aria-current]:not([aria-current="false"]). That predicate matches aria-current="", but ARIA says an empty value is not current.

MDN's aria-current reference states: "Any non-null string value not included in this list of enumerated values is treated as if aria-current="true" were set, not the default false value. If the attribute is not present, is an empty string, is present with no value, or is set to aria-current="false" it is not exposed to the user."

So the current predicate is right about unknown tokens (aria-current="bogus" is current) and wrong about the empty token.

Evidence

Verified in Chromium (Playwright 1.63.0) against the four value shapes:

[aria-current]:not([aria-current='false'])
  aria-current=""       -> matches     (should not)
  aria-current="page"   -> matches     (correct)
  aria-current="bogus"  -> matches     (correct)
  aria-current="false"  -> no match    (correct)
  attribute absent      -> no match    (correct)

The affected declarations:

https://github.com/ariakit/ariakit/blob/211254f443699aede1a6668d51b774089d2dbd4b/packages/ariakit-ui/src/styles/ui.css#L611-L640

Is this pre-existing on main?

Yes, verified. origin/main is at 211254f, and git show origin/main:packages/ariakit-ui/src/styles/ui.css carries the identical predicate in both ui-selected (lines 611 to 621) and ui-selected-within (lines 623 to 640). This does not depend on #7536 and does not need to wait for it.

Blast radius

ui-selected is consumed on main by packages/ariakit-ui/src/styles/control.ts, glider.ts, table.ts, and tabs.ts. #7536 adds selected.ts and nav.ts to that list, and inlines a verbatim copy of the predicate in nav.ts and disclosure.ts so that a glider and a disclosure guide land on exactly what the item paints.

Those copies are why a partial fix is worse than none: excluding the empty value in one place while the shared variant still accepts it yields an item that paints as current with no glider on it. The shared variant and every inlined copy have to change together.

Why #7536 can merge without this

The predicate is unchanged by that pull request, its stated outcome does not depend on the empty-token case, and no fixture or test uses aria-current="". Reaching the bug from React takes deliberate authoring: aria-current={undefined} omits the attribute and aria-current={false} renders "false", so only an explicit empty string triggers it.

Expected outcome

aria-current="" stops reading as current everywhere, and unknown tokens keep reading as current.

Concretely: narrow the predicate in ui-selected and ui-selected-within to exclude the empty value, update every inlined copy in the same change, and add coverage that pins both halves of the ARIA rule (empty is not current, unknown token is current) so the two do not drift apart again.