Skip to content

Support focusable aria-disabled-only buttons (do not force native disabled)#2846

Closed
KAMRONBEK wants to merge 1 commit into
necolas:masterfrom
KAMRONBEK:fix-aria-disabled-only
Closed

Support focusable aria-disabled-only buttons (do not force native disabled)#2846
KAMRONBEK wants to merge 1 commit into
necolas:masterfrom
KAMRONBEK:fix-aria-disabled-only

Conversation

@KAMRONBEK

Copy link
Copy Markdown

Problem

It is impossible to render the ARIA-recommended focusable disabled control, e.g. <button aria-disabled="true">. With <Pressable aria-disabled role="button"> the attribute is stripped from the DOM entirely, and passing aria-disabled to any element rendered as button/form/input/select/textarea also forces the native disabled attribute, which removes the element from the tab order and prevents screen-reader users from discovering it and receiving feedback.

Root cause

Two coupled defects:

  1. createDOMProps merged the props (const disabled = ariaDisabled || accessibilityDisabled) and, when true, set both aria-disabled="true" and the native disabled attribute for button/form/input/select/textarea element types. There was no way to get the ARIA attribute without the native one.
  2. Pressable rendered aria-disabled={disabled} after {...rest}, so a user-supplied aria-disabled was clobbered with undefined whenever the disabled prop was not set — which is why the attribute disappears from the DOM completely in the issue's repro.

Fix

  • createDOMProps: the native disabled attribute is now only set when accessibilityDisabled === true; aria-disabled alone emits just aria-disabled="true", keeping the element focusable and perceivable by assistive technology. When accessibilityDisabled is set (with or without aria-disabled) the output is unchanged: both attributes, exactly as before.
  • Pressable: passes its disabled prop through as accessibilityDisabled (mirroring what TouchableOpacity/TouchableHighlight/TouchableWithoutFeedback already do), so <Pressable disabled role="button"> keeps producing byte-identical DOM (aria-disabled="true" disabled tabindex="-1", pointer-events/press/hover gating untouched), while a user-supplied aria-disabled now flows through untouched: <Pressable aria-disabled role="button"> renders <button aria-disabled="true" role="button" tabindex="0" type="button"> with onPress still firing — matching React Native's behavior on iOS/Android described in the issue.

Tests

  • createDOMProps: accessibilityDisabled on button → aria-disabled + disabled (as today); aria-disabled only → aria-disabled without disabled; both props → both attributes (as today).
  • Pressable: snapshot + attribute assertions for disabled with role="button" (locks current behavior) and aria-disabled with role="button" (new pattern).
  • All 58 pre-existing snapshots pass unchanged (no DOM change for existing disabled/accessibilityDisabled users). The new aria-disabled-only tests fail without the fix. flow, prettier, and eslint pass.

Note for maintainers

The aria-disabled → native disabled coupling in createDOMProps looks deliberate ("Enhance with native semantics"), so this PR is a semantic decision as much as a bug fix: it implements the ARIA authoring-practices pattern where aria-disabled communicates the disabled state while keeping the control focusable, and reserves the native attribute for disabled/accessibilityDisabled. Happy to adjust if you prefer a different split (e.g. keying the native attribute off a different prop).

Fixes #2736

createDOMProps coupled 'aria-disabled' to the native 'disabled'
attribute: any element rendered as button/form/input/select/textarea
with 'aria-disabled' also received 'disabled', which removes it from
the tab order and blocks screen-reader interaction. This made the
ARIA-recommended focusable aria-disabled-only pattern impossible.

Pressable also rendered 'aria-disabled={disabled}' after the prop
spread, clobbering a user-supplied 'aria-disabled' with undefined
when 'disabled' was not set, so the attribute was stripped entirely.

Gate the native 'disabled' attribute on 'accessibilityDisabled' and
pass Pressable's 'disabled' through as 'accessibilityDisabled' (as
the Touchables already do). 'disabled'/'accessibilityDisabled' keep
producing both attributes exactly as before; only 'aria-disabled'
alone now emits just the ARIA attribute.

Fix necolas#2736
@codesandbox-ci

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 8308187:

Sandbox Source
react-native-web-examples Configuration

@KAMRONBEK

Copy link
Copy Markdown
Author

@necolas TL;DR for #2736: createDOMProps coalesces aria-disabled and accessibilityDisabled into one flag, so an aria-disabled-only button always gets the native disabled attribute — removing it from tab order and SR announcement, making the ARIA-recommended focusable aria-disabled pattern impossible. This distinguishes the two: aria-disabled alone emits only the attribute; disabled/accessibilityDisabled keeps today's behavior exactly (incl. combined usage). This intentionally changes the current coupling for the aria-only case — flagged in the body, and I'd welcome your guidance if you'd prefer different semantics.

@KAMRONBEK KAMRONBEK closed this Jul 17, 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.

Impossible to create a button with only aria-disabled

1 participant