Disabled buttons: use disabled (and aria-disabled) the right way
Common bug Making a control look disabled with CSS, while leaving it focusable and activatable.
Goal If the UI communicates “you can’t do this right now”, then keyboard and assistive tech users should not be able to trigger the action by accident.
Interactive demo: disabled-buttons-demo.html
Use disabled on real buttons
For a native <button>, prefer the built-in disabled attribute. Benefits:
- The button becomes non-interactive.
- It is removed from the tab order (so keyboard users don’t waste time landing on a control they can’t use).
- Assistive technologies can reliably understand the state.
Provide a reason, not just a state
“Disabled” answers what, not why. Add a short explanation near the control and associate it with aria-describedby so it’s discoverable.
When aria-disabled is appropriate
Sometimes you can’t use disabled (for example: a link styled as a button, or a custom widget in a component library). In those cases:
- Add
aria-disabled="true"to expose the disabled state. - Remove it from the tab order while disabled (often
tabindex="-1"). - Prevent activation in code (cancel click/keyboard activation).
- Consider adding a reason with
aria-describedby, same as with buttons.
Why CSS-only disabling is insufficient
- Focus mismatch: it may still be reachable via , implying it can be used.
- Action mismatch: it may still fire on click/Enter/Space.
- AT mismatch: screen readers may announce it as an enabled button, contradicting the visuals.
Checklist
- For
<button>: usedisabled, not CSS-only styling. - When disabled: ensure the action cannot trigger (mouse, keyboard, programmatic).
- Explain why it’s disabled, and connect the explanation with
aria-describedby. - For non-button controls: use
aria-disabled+ remove from tab order + prevent activation. - If state changes dynamically: announce politely (e.g.,
aria-live="polite") without being spammy.