Key insights
- A disabled button drops out of the tab order, so keyboard users skip right past it and screen readers stay silent. The block exists, but nothing announces it.
- Pointer events are dead on a disabled element, so a tooltip meant to explain the block never fires. The reason is unreachable by design.
- Greyed-out labels usually fail contrast. A disabled state can land near 1.9:1, well under the 4.5:1 threshold, so the text is hard to read on top of being blocked.
- Keep the button live and validate on click instead. Light up the fields that are blocking submit, then move focus to the first one so the path forward is visible.
- Disabled and loading are different states. During a request, hold focus, show a spinner, and report
aria-busy; greying the button out throws the user's place away.
Do / Don't
- Do: Keep the button enabled, validate on click, then flag the blocking fields and move focus to the first one.
- Do: For async actions, use a busy state that holds focus, spins, and sets aria-busy.
- Do: Name the blocker in reachable text, not a tooltip attached to a dead control.
- Don't: Disable submit and leave the user to guess what is missing.
- Don't: Rely on a tooltip to explain a disabled control, since pointer events never fire on it.
- Don't: Treat loading as disabled; greying out mid-request drops focus and the user's place.