Color Only State

Status: ready

Symptom

A state is shown only by color. The selected filter is blue, the invalid field has a red border, the paid order is green, the selected payment method has a tint. VoiceOver reads every option the same way.

Delivery, button
Pickup, button
Card, button
Apple Pay, button

Which one is selected? The app knows. The user doesn't.

Where it shows up

Filters, tabs, segmented controls, form validation, payment status, charts, maps, availability badges, ratings, order status.

Why it blocks the task

About 1 in 12 men has some form of color vision deficiency. Add low vision, sunlight on the screen, Grayscale and VoiceOver, and a color-only state is invisible or silent for a large share of users. In checkout, "which payment method is selected" decides what gets charged.

Root cause

The design treats color as data. The code has the state but exposes it only as a style.

  1. The state is only in foreground or background color.
  2. The selected item has no .isSelected trait.
  3. Error state is only a red border.
  4. A chart legend is color-only.
  5. Nobody turned on Differentiate Without Color.

Before

Text("Open now")
    .foregroundStyle(isSelected ? .blue : .primary)

VoiceOver says "Open now". Selected or not, the same.

Fix

Verdict: a second visual signal (icon, text or shape) for everyone, plus the platform's semantic state for assistive technology. For selection that's the .isSelected trait, not a value string.

Why: the icon works without color for every user. The trait makes VoiceOver say "Selected" in the user's language and Braille shows it. accessibilityValue("Selected") on top of the trait reads the state twice.

SwiftUI:

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

VoiceOver reads "Selected" together with "Open now, button" on the selected filter and only "Open now, button" on the others. The exact position of "Selected" in the phrase depends on the iOS version, so a test checks that it's there, not where.

For on/off settings use Toggle, not a button with a selected trait. For an error, the message is text next to the field and in the field's value (Silent Error).

UIKit:

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

Differentiate Without Color is a user setting. Respect it where the default design still relies on color, but don't make the second signal depend on it:

@Environment(\.accessibilityDifferentiateWithoutColor) private var noColor

Check

Profile:   VoiceOver
Task:      choose Card as the payment method
Expected:  each option has a stable name
           only the selected option says "Selected"
           "Selected" is spoken once, not twice
Profile:   Grayscale on (Settings → Accessibility → Display & Text Size → Color Filters)
Task:      find the selected filter and the invalid field
Expected:  both are identifiable without color

WCAG mapping

  1. 1.4.1 Use of Color: color isn't the only signal.
  2. 1.3.1 Info and Relationships: the state is available programmatically.
  3. 4.1.2 Name, Role, Value: the selected state is exposed.
  4. 1.4.11 Non-text Contrast: the state indicator has 3:1 contrast.

App Store and product risk

This is the evidence behind the Differentiate Without Color Alone label. If filters, payment state or charts rely on color, the label can't be claimed.

  1. State as Value
  2. Semantic Control
  3. Testing Evidence Record
  1. Chapter 4
  2. Chapter 6
  3. Chapter 13
  4. Chapter 17

Sources

  1. Apple Developer: AccessibilityTraits.isSelected
  2. Apple Human Interface Guidelines: Color
  3. Apple Support: Differentiate Without Color
  4. W3C: Understanding SC 1.4.1 Use of Color