Implementing Semantic Labels for Mobile UI Elements
Semantic Labels — not just accessibilityLabel. It's a properly structured system of descriptions, hints, roles and states for each interface element that screen reader announces to user. Difference between "button" (VoiceOver reads something by default) and "Add to favorites, button, not activated" — difference between usable and unusable application.
What Complete Element Description Consists Of
Label, Hint, Trait, Value
On iOS these are four separate properties of UIAccessibilityElement:
- Label — what this is: "Ivan Petrov's profile photo", "Send button"
- Hint — what happens on activation (optional if label is self-sufficient): "Opens profile page"
-
Traits — type and state:
.button,.link,.image,.header,.selected,.notEnabled - Value — current value for stateful elements: slider "volume 70%", toggle "on"
VoiceOver announces them in order: label → value → traits → hint. Pause between label and value, pause before hint.
In SwiftUI: .accessibilityLabel(), .accessibilityHint(), .accessibilityAddTraits(), .accessibilityValue().
On Android equivalent: contentDescription (= label + value), AccessibilityNodeInfo.setRoleDescription() (= custom trait), stateDescription (API 30+).
Composite Elements
Product card with image, name, price and "Add to Cart" button:
Bad: VoiceOver focuses on each subview separately — 4 steps to reach button. Screen reader reads: "image", "Nike Air Max 90", "8,900 rubles", "button".
Correct: combine into single element with composite label: "Nike Air Max 90, 8,900 rubles" + trait .button + hint "Adds to cart". Plus separate element — "Add to Cart" button — if user needs quick access without reading entire card.
In UIKit: containerView.accessibilityElements = [productAccessibilityElement, addToCartButton]. productAccessibilityElement — custom UIAccessibilityElement with needed accessibilityFrame and label from multiple fields.
Dynamic States
"Favorite" button — heart icon, no text, changes state. Base label "Add to favorites". On addition — need update:
- iOS:
accessibilityLabel = "Remove from favorites", or viaaccessibilityTraits.insert(.selected)+ label "Favorite" (VoiceOver adds "selected") - After change:
UIAccessibility.post(notification: .announcement, argument: "Added to favorites")— instant notification without refocusing
Toggles (UISwitch, Toggle in SwiftUI, Switch in Compose) — state announced automatically: "Notifications, on". For custom toggles — need set accessibilityValue = isOn ? "on" : "off".
Section Headers
Screen with sections: accessibilityTraits = .header on section header — VoiceOver user can move between headers via swipe with "Headings" rotor selection. Without this can't quickly jump to needed section without linear traversal of all elements.
In Compose: Modifier.semantics { heading() } on Text-header.
Typical Mistakes
Icon buttons — contentDescription = "ic_heart" (resource filename) instead of "Add to favorites". Android Studio warns but developers often leave as is.
Placeholder in TextField as label: hint = "Enter email" in Android EditText — TalkBack reads hint as contentDescription only if contentDescription not set. On focus hint disappears and description vanishes. Need explicit contentDescription or use TextInputLayout with floating hint.
Buttons with number badges: "3" next to bell icon — screen reader reads "3, button" without context. Need: accessibilityLabel = "Notifications, 3 unread".
Timeframe: 1-3 days depending on number of components. Often combined with VoiceOver/TalkBack audit. Cost calculated individually.







