How does VoiceOver read your UI: accessibility elements, labels, values, hints and traits, and how do you fix an icon-only button?

24.1.1 · L2 · Interview

Predict first

What are the raw values of the common traits, and is VoiceOver running in a plain process?

import UIKit

print(UIAccessibilityTraits.button.rawValue, UIAccessibilityTraits.header.rawValue, UIAccessibilityTraits.selected.rawValue, UIAccessibilityTraits.adjustable.rawValue, UIAccessibilityTraits.notEnabled.rawValue)
print(UIAccessibility.isVoiceOverRunning)
  • 1 65536 8 4096 256, then false
  • 0 1 2 3 4, then true
  • 1 2 4 8 16, then false

Predict first

Does a SwiftUI card with a hidden decorative image, a header, a labelled icon button with a hint, and an explicit order compile?

import SwiftUI

struct Card: View {
    var body: some View {
        HStack {
            Image(systemName: "star").accessibilityHidden(true)
            Text("Weekly report").accessibilityAddTraits(.isHeader)
            Button { } label: { Image(systemName: "square.and.arrow.up") }
                .accessibilityLabel("Share")
                .accessibilityHint("Sends the report to someone")
        }
        .accessibilityElement(children: .contain)
        .accessibilitySortPriority(1)
    }
}
  • Yes
  • No, SwiftUI has no accessibility modifiers

Mental model. A screen reader is a tour guide describing a room to someone who cannot see it. “Share, button” is a good description; “icon_share_2” and “image” are a guide who has forgotten the names.

VoiceOver does not read pixels; it reads the accessibility tree: a list of accessibility elements, each with properties it speaks in a fixed order, the label (what it is: “Share”), the value (its current state: “50 percent”, “on”), the traits (what kind of thing it is: button, header, selected, adjustable, not enabled, link) and finally an optional hint (what happens: “Sends the report to someone”). Standard controls fill most of this in for you, so a UIButton or SwiftUI Button with a text title has its label and the button trait already; a UISwitch reports its value. The work is in the gaps: an icon-only button has no text, so add a label (accessibilityLabel in UIKit, .accessibilityLabel("Share") in SwiftUI); a custom-drawn control needs isAccessibilityElement = true plus the right label, value and traits; decorative images should be hidden (.accessibilityHidden(true)) so they do not add noise. The traits you can set are plain flags (the check below prints their raw values), and the most useful are .header (lets users jump between headings), .selected, .notEnabled and .adjustable.

Good labels are short, say what the element is or does and do not include the control type (“Share”, not “Share button”: the trait already says “button”) or the state (that is the value). Hints are for results that are not obvious and should not repeat the label. Group related pieces so one swipe reads a card as one thing (.accessibilityElement(children: .combine)), keep an element’s children separate with .contain when each part is actionable, and fix the order when it does not follow the visual flow (accessibilitySortPriority in SwiftUI, accessibilityElements in UIKit). Always test with VoiceOver on a device: close your eyes, or use the screen curtain, and try to complete a task. Localise labels along with the UI (Q109).

Traits are bit flags (button is 1, selected 8, adjustable 4096 and so on). VoiceOver is a system setting that is off here.

These are the standard modifiers for labels, hints, traits, hiding and ordering.

Common traps

  • Putting “button” in the label. VoiceOver already says it from the trait, so it is read twice.
  • Leaving icon-only buttons unlabelled. VoiceOver then says just “button” and the user cannot tell what it does.

They might follow up with

  • What is the spoken order? Label, value, traits, then hint.
  • How do you stop VoiceOver reading a decorative image? Hide it from accessibility, so it is not an element.

Related questions

  • 24.1.2 How do custom actions, adjustable controls, rotors and announcements make complex UI usable with VoiceOver?
  • 24.1.3 How do Voice Control, Switch Control and keyboard access use your UI, and what do accessibility input labels…
  • 24.5.1 How do you test accessibility: the Accessibility Inspector, automated audits in XCUITest, identifiers versus…
  • 2.6.3 How do you localise strings: Localizable.strings, string catalogs, String(localized:)?
  • 22.5.2 How do you write UI tests that stay stable: accessibility identifiers, page objects, launch arguments…

How do custom actions, adjustable controls, rotors and announcements… →