Skip to content

[button] Enable buttons to remain focusable when disabled#48613

Open
mj12albert wants to merge 5 commits into
mui:masterfrom
mj12albert:button-focusable-when-disabled
Open

[button] Enable buttons to remain focusable when disabled#48613
mj12albert wants to merge 5 commits into
mui:masterfrom
mj12albert:button-focusable-when-disabled

Conversation

@mj12albert

@mj12albert mj12albert commented Jun 3, 2026

Copy link
Copy Markdown
Member

Closes #32917

Preview: https://deploy-preview-48613--material-ui.netlify.app/material-ui/react-button/#loading-2

Screenshot 2026-06-17 at 7 53 31 PM

In the default styles, the disabled styles suppress the focus styles completely; an alt focus style (added a focus ring for this) is needed in order for "focusable when disabled" to be visually identifiable

@mj12albert mj12albert added type: new feature Expand the scope of the product to solve a new problem. scope: icon button Changes related to the icon button. scope: button Changes related to the button. labels Jun 3, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Jun 3, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+1.99KB(+0.38%) 🔺+424B(+0.28%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 4, 2026
@mj12albert mj12albert force-pushed the button-focusable-when-disabled branch from af55452 to 4cf24e7 Compare June 8, 2026 15:00
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 8, 2026
@mj12albert mj12albert force-pushed the button-focusable-when-disabled branch from 4cf24e7 to e18f541 Compare June 11, 2026 15:53
@mj12albert mj12albert marked this pull request as ready for review June 17, 2026 11:50
Comment on lines +213 to +221
For disabled `Button` and `IconButton` components that need to trigger a Tooltip, use the `focusableWhenDisabled` prop instead:

```jsx
<Tooltip title="You don't have permission to do this">
<Button disabled focusableWhenDisabled>
Disabled
</Button>
</Tooltip>
```

@siriwatknp siriwatknp Jun 18, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this section should be outside of "Cursor not-allowed" or just link to a new section that explain the focusableWhenDisabled prop.

@siriwatknp siriwatknp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Picking this up to continue the work.

Before going further, want to lock the scope of focusableWhenDisabled so we are aligned.

Putting the intended behavior and styling matrix here, so the discussion has one reference.

The idea: a disabled + focusableWhenDisabled element is reachable but inert. It can take focus and receive hover/pointer events, so assistive tech finds it and Tooltip can explain why it is off. But it must still look and act not-actionable.

Behavior

Capability enabled disabled (native) disabled + focusableWhenDisabled
In tab order / focusable
Programmatic .focus()
Receives hover/pointer events pointer-events:none pointer-events:auto
Tooltip on hover ❌ (needs <span> wrapper)
Tooltip on keyboard focus
onClick fires ❌ blocked
Keyboard activate (Enter/Space) ❌ blocked
Event bubbles to ancestor stopPropagation
onMouseLeave / onBlur fire ✅ (so Tooltip can close)
Announced to AT as disabled via disabled attr via aria-disabled="true"
Applies to links (href/to) ❌ links stay non-focusable

Styling (disabled + focusableWhenDisabled)

Visual treatment Show? Why
Disabled/dimmed base (action.disabled) it is disabled
Cursor default not actionable, keep production behavior
Hover background / elevation inert, would falsely signal clickable
Active / pressed style not actionable
Ripple on press/tap ripple means "did something"
Focus-visible ring out of scope this PR rely on existing focus-visible style, verify it stays visible (WCAG 2.4.7), else separate follow-up

Scope calls

  1. Focus ring: drop it from this PR. The focus-ring CSS variables and outline are a separate concern. Keep this PR to behavior only.
  2. Cursor: keep the production behavior (default). No change.
  3. Hover: prevent Material UI built-in hover styles when disabled. The :hover:not(.Mui-disabled) approach is right.

props: { focusableWhenDisabled: true },
style: {
[`&.${buttonClasses.disabled}.${buttonClasses.focusVisible}`]: {
outline: '2px solid var(--Button-focusRingColor)',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to add the focus ring here? --Button-focusRingColor plus outline looks like a separate focus-ring effort, not part of the disabled-focusable behavior.

Please drop it from this PR and keep Button scoped to behavior.

Two follow-ons once it is gone:

  • likely can drop the focusableWhenDisabled ownerState and shouldForwardProp plumbing in the styled root too.
  • please confirm a disabled-focusable button still shows a visible focus indicator from the existing focus-visible style. If not, that is a separate accessibility follow-up, not a reason to add a ring system here.

props: { focusableWhenDisabled: true },
style: {
[`&.${iconButtonClasses.disabled}.${buttonBaseClasses.focusVisible}`]: {
outline: '2px solid var(--IconButton-focusRingColor)',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as Button — please drop --IconButton-focusRingColor and the outline from this PR. Separate concern, keep IconButton scoped to behavior.

// Backward compatibility: `preventDefault()` inside `onClose` used to stop later
// Snackbars from handling the same Escape event. Preserve that documented behavior
// without letting unrelated, pre-existing `defaultPrevented` values suppress Snackbar.
if (!defaultPreventedBeforeClose && nativeEvent.defaultPrevented) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this misses the already-prevented case. When Escape reaches this listener already defaultPrevented (a focused child prevented it), defaultPreventedBeforeClose is already true, so the first Snackbar onClose calling preventDefault() cannot set defaultMuiPrevented.

From what I traced, with multiple open Snackbars they all close instead of one-at-a-time.

Please add a regression test for the pre-prevented + multi-snackbar case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@mj12albert

mj12albert commented Jun 18, 2026

Copy link
Copy Markdown
Member Author

Is there a reason to add the focus ring here?

I was trying to do it lightly here (this was before you started the theme-level focus ring config) because just opening up the focusableWhenDisabled prop without providing any styling means that the "focused when disabled" state has no visual indication, since the disabled styling suppresses the focus style completely.

Originally I added the focus ring only in a few demos, but it doesn't seem like a good DX for this feature to require users to add their own styles in order to be fully functional

  • please confirm a disabled-focusable button still shows a visible focus indicator from the existing focus-visible style

The disabled style "grays out" the whole button, so it's impossible for both these styles to coexist without some explicit design change/a new visual affordance:

Screenshot 2026-06-18 at 8 26 54 PM Screenshot 2026-06-18 at 8 25 13 PM

Maybe we should try to do the library/theme-level focus ring first, WDYT? @siriwatknp @silviuaavram

@mj12albert mj12albert force-pushed the button-focusable-when-disabled branch from e18f541 to 7854857 Compare June 18, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: button Changes related to the button. scope: icon button Changes related to the icon button. type: new feature Expand the scope of the product to solve a new problem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[button] Allow receiving focus when disabled

2 participants