Silent Error

Status: ready

Symptom

An error appears on screen, but the user doesn't get a way to recover. VoiceOver focus stays on Pay, the field turns red, a toast disappears after 2 seconds, and the user doesn't know what to fix.

Where it shows up

Checkout, login, payment, account recovery, support requests, address forms, any form with validation.

Why it blocks the task

A sighted touch user sees the red border. A VoiceOver, Switch Control or keyboard user is still on the button they just pressed and hears nothing new. If the entered data was also cleared, the task is a dead end: the user doesn't know what failed and has to type everything again.

Root cause

The error is drawn as decoration, not as part of the task flow.

  1. Focus doesn't move to the error or the invalid field.
  2. The error text says what's wrong but not what to do.
  3. The field doesn't carry the error, so returning to it says nothing.
  4. The error lives in a toast that disappears on a timer.
  5. Input is cleared after the failure.

Before

Text("Invalid date")
    .foregroundStyle(.red)

After tapping Pay, VoiceOver says nothing new. Focus is still on:

Pay, button

The error is in the tree, but somewhere above the current position.

Fix

Verdict: after a failed submit, focus moves to the error. The error text says what to do. The field keeps its value and carries the error. Nothing important lives only in a toast.

Why: the user lands where the correction starts and hears the field, the problem and the next step without searching the screen.

SwiftUI:

@State private var expiration = "12/19"
@State private var expirationError: String?
@AccessibilityFocusState private var errorFocused: Bool

VStack(alignment: .leading) {
    TextField("Expiration date", text: $expiration, prompt: Text("MM/YY"))
        .accessibilityLabel("Expiration date")
        .accessibilityValue(expirationError.map { "\($0). \(expiration)" } ?? expiration)

    if let expirationError {
        Text(expirationError)
            .foregroundStyle(.red)
            .accessibilityFocused($errorFocused)
    }
}

func submit() {
    expirationError = validate(expiration)   // "Card expired. Enter a date after 11/26."
    if expirationError != nil { errorFocused = true }
}

VoiceOver after Pay:

Card expired. Enter a date after 11/26.

One swipe further, the field:

Expiration date, Card expired. Enter a date after 11/26. 12/19, text field

UIKit, error on one field:

errorLabel.text = "Card expired. Enter a date after 11/26."
errorLabel.isHidden = false
expirationField.accessibilityValue = "Card expired. 12/19"
UIAccessibility.post(notification: .layoutChanged, argument: errorLabel)

UIKit, error for the whole form: the same, with a summary label at the top as the argument. .layoutChanged is for a change within the screen. .screenChanged is for a new screen or modal.

What we don't do

  • We don't clear what the user typed.
  • We don't put the only copy of an error in a toast.
  • We don't use accessibilityHint for the error. Users can turn hints off.
  • We don't announce the error and leave focus on Pay. The announcement is gone in 2 seconds, the user still has to find the field.

Check

Profile:   VoiceOver
Task:      pay with an expired card
Expected:  after Pay, focus is on the error, which is read once
           the error says what to enter
           the field still has 12/19
           swipe right reaches the field, which reads the error in its value
Profile:   Switch Control
Task:      fix the date and pay again
Expected:  the field is reachable without scanning the whole form
           the error disappears after a valid date

WCAG mapping

  1. 3.3.1 Error Identification: the error is identified in text.
  2. 3.3.3 Error Suggestion: the text says how to fix it.
  3. 2.4.3 Focus Order: recovery follows the task.
  4. 1.3.1 Info and Relationships: the error is tied to the field.

App Store and product risk

Rarely a rejection on its own. It's a lost order: a user who can't recover from a declined card leaves. A team that claims VoiceOver support for checkout without testing the error path has no evidence for the claim.

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

Sources

  1. Apple Developer: AccessibilityFocusState
  2. Apple Developer: accessibilityFocused
  3. Apple Developer: UIAccessibility.Notification
  4. W3C: Understanding SC 3.3.1 Error Identification