State as Value

Status: ready

Intent

Any state the user can see can also be heard: selected, on, off, count, progress, unavailable, invalid, rating.

Problem

The UI shows state with color, an icon, an animation or a position. VoiceOver hears only the name.

Card, button
Cash, button
Apple Pay, button

Which one is selected?

Solution

Verdict: the name goes in the label and doesn't change. The state goes in a trait when the platform has one, and in the value when it doesn't.

Why: the label answers "what is it", the value answers "what's its state now". A trait is spoken in the user's language and shown on a Braille display without any extra work. A state string in the label turns the name into a sentence that changes, which breaks Voice Control commands.

Question Where
What is it? label
What state is it in? Trait (.isSelected, .notEnabled, Toggle), otherwise value
What kind of control? Trait (.isButton, .isHeader, adjustable)
What happens if I activate it? hint, only if it isn't obvious

Selected is a trait. Don't add accessibilityValue("Selected") on top, or VoiceOver says it twice.

Implementation

Selected filter, SwiftUI:

Button {
    filter.isOpenNow.toggle()
} label: {
    Label("Open now", systemImage: filter.isOpenNow ? "checkmark.circle.fill" : "circle")
}
.accessibilityAddTraits(filter.isOpenNow ? .isSelected : [])

On/off: a Toggle, not a button with state.

Toggle("Marketing emails", isOn: $marketingEmails)

Count:

CartButton()
    .accessibilityValue("\(cart.count) items")

Rating, one adjustable value instead of five star buttons:

Stepper("Rating", value: $rating, in: 1...5)
    .accessibilityValue("\(rating) of 5")

UIKit, selected cell:

cell.accessibilityLabel = "Card"
cell.accessibilityTraits = isSelected ? [.button, .selected] : [.button]

UIKit, progress:

statusView.accessibilityLabel = "Order status"
statusView.accessibilityValue = "Preparing. 18 minutes left"
statusView.accessibilityTraits.insert(.updatesFrequently)

Value strings are user-facing text. Localize them like any other string. A trait is localized by the system.

Check

Profile:   VoiceOver
Task:      choose Card as the payment method, then go through the options again
Expected:  each option keeps the same name
           only Card says "Selected", once
           the state changes right after activation
Profile:   Grayscale, Differentiate Without Color on
Task:      find the selected filter
Expected:  visible without color: icon, text or shape
Profile:   Arabic locale
Task:      the same screen
Expected:  value strings are translated, the selected indicator mirrors correctly

Pitfalls

  1. "Selected" as a value plus .isSelected. Spoken twice.
  2. State in the label: "Card selected". Voice Control's command changes with the state.
  3. State only in the hint. Users can turn hints off.
  4. .isSelected on everything that looks highlighted. Only on the actually selected item.
  5. Five rating buttons when the task is choosing one number.
  6. Hardcoded English value strings.

WCAG mapping

  1. 4.1.2 Name, Role, Value: the current state is exposed.
  2. 1.4.1 Use of Color: the state isn't only color.
  3. 1.3.1 Info and Relationships: visual state maps to semantics.
  4. 1.4.11 Non-text Contrast: state indicators have 3:1 contrast.
  1. Color Only State
  2. Silent Error
  3. Animation Only Feedback
  4. Announcement Spam
  5. Duplicate Voice Command
  6. Skeleton Noise
  7. Disabled Mystery
  1. Chapter 4
  2. Chapter 5
  3. Chapter 6
  4. Chapter 11
  5. Chapter 17

Sources

  1. Apple Human Interface Guidelines: Color
  2. Apple Developer: AccessibilityTraits.isSelected
  3. Apple Developer: UIAccessibilityTraits updatesFrequently
  4. Apple Support: Differentiate Without Color