Disabled Mystery

Status: ready

Symptom

The primary button is disabled and nothing says why. Visually it's gray. VoiceOver says "dimmed". The user has to guess which field is missing.

Continue, dimmed, button

Where it shows up

Forms, checkout, onboarding, account creation, support tickets, rating screens.

Why it blocks the task

The user has to find the missing requirement by inspecting every field. With VoiceOver, the problem field may be 10 swipes above the button, and nothing connects the two. A disabled button can't be pressed, so it can't trigger validation that would show the error. The user is stuck in front of a locked door with no sign.

Root cause

The validation exists in code, but not in the UI.

  1. "Disabled" is treated as an explanation.
  2. Required fields aren't marked as required.
  3. The button knows the form is invalid. The user doesn't.
  4. The reason is only in color or a tooltip.

Before

Button("Continue") { submit() }
    .disabled(!form.isValid)

Fix

Verdict: keep the button enabled. Pressing it validates, shows the errors and moves focus to the first one. Disable a button only when the reason is visible next to it.

Why: an enabled button turns "guess what's wrong" into "here's what's wrong". The user gets a specific error with a fix instead of a gray rectangle.

SwiftUI, enabled with validation:

Button("Continue") {
    errors = form.validate()
    if errors.isEmpty { submit() } else { errorFocused = true }
}

Error focus and field values: Silent Error.

When disabled is the right choice (for example, a terms checkbox that must be checked), the reason is visible and is part of the button:

VStack(alignment: .leading) {
    Text("Accept the terms to continue.")
        .font(.footnote)
    Button("Continue") { submit() }
        .disabled(!termsAccepted)
        .accessibilityValue(termsAccepted ? "" : "Accept the terms first")
}

The visible text is for everyone. The value puts the reason on the button itself, so a VoiceOver user hears it together with "dimmed" without searching. A hint would also work, but users can turn hints off.

UIKit:

continueButton.isEnabled = termsAccepted
continueButton.accessibilityValue = termsAccepted ? nil : "Accept the terms first"

What we don't do

  • We don't announce "Continue is now available" on every keystroke. If the user is typing, they'll get to the button.
  • We don't explain the reason only in color, a tooltip or VoiceOver. The visible text comes first.
  • We don't use a card length as a rule ("16 digits"). American Express cards have 15. Validate with the card network rules.

Check

Profile:   VoiceOver
Task:      open the form, leave the email empty, press Continue
Expected:  Continue can be pressed
           focus moves to the error for the email field
           entered values stay
Profile:   VoiceOver
Task:      a disabled Continue on the terms screen
Expected:  the reason is visible on screen
           the reason is reachable from the button without searching
Profile:   Larger Text, AX5
Task:      the same screens
Expected:  the reason stays visible and next to the button

WCAG mapping

  1. 3.3.2 Labels or Instructions: requirements are explained before or near the input.
  2. 3.3.1 Error Identification: the invalid field is identified.
  3. 3.3.3 Error Suggestion: the user gets a fix.

App Store and product risk

A blocked checkout or account form is a lost user. If the VoiceOver label is claimed for that flow, it has to work without guessing.

  1. Disabled State Explanation
  2. Focus Recovery
  3. State as Value
  4. Testing Evidence Record
  1. Chapter 4
  2. Chapter 9
  3. Chapter 13
  4. Chapter 17

Sources

  1. Apple Human Interface Guidelines: Buttons
  2. Apple Developer: UIAccessibilityTraits notEnabled
  3. W3C: Understanding SC 3.3.2 Labels or Instructions