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
- "Selected" as a value plus
.isSelected. Spoken twice. - State in the label: "Card selected". Voice Control's command changes with the state.
- State only in the hint. Users can turn hints off.
.isSelectedon everything that looks highlighted. Only on the actually selected item.- Five rating buttons when the task is choosing one number.
- Hardcoded English value strings.
WCAG mapping
4.1.2 Name, Role, Value: the current state is exposed.1.4.1 Use of Color: the state isn't only color.1.3.1 Info and Relationships: visual state maps to semantics.1.4.11 Non-text Contrast: state indicators have 3:1 contrast.
Related smells
- Color Only State
- Silent Error
- Animation Only Feedback
- Announcement Spam
- Duplicate Voice Command
- Skeleton Noise
- Disabled Mystery