How to Add Accessibility Labels in SwiftUI
Attach .accessibilityLabel("…") to any SwiftUI view to give VoiceOver a clear spoken name. Pair it with .accessibilityHint("…") to describe the action, and use .accessibilityHidden(true) to silence purely decorative elements.
Button(action: likePost) {
Image(systemName: "heart.fill")
.foregroundStyle(.red)
}
.accessibilityLabel("Like post")
.accessibilityHint("Double-tap to like this post")
// Decorative divider — VoiceOver skips it
Divider()
.accessibilityHidden(true)
Full implementation
The example below builds a social-style post card. Icon-only buttons get explicit .accessibilityLabel strings, a like-count value is surfaced with .accessibilityValue, and the avatar image is hidden from VoiceOver because the author's name is already announced by the adjacent Text view. The action row groups its children so VoiceOver treats each button as one tap target rather than traversing label and icon separately.
import SwiftUI
struct PostCard: View {
let author: String
let body: String
var likeCount: Int
@State private var liked = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// MARK: Header
HStack(spacing: 10) {
Image(systemName: "person.circle.fill")
.font(.title2)
.foregroundStyle(.secondary)
// Decorative: author name Text already provides context
.accessibilityHidden(true)
Text(author)
.font(.headline)
// Explicit label clarifies the element's role
.accessibilityLabel("Author: \(author)")
}
// MARK: Post body
Text(body)
.font(.body)
.foregroundStyle(.primary)
// Body text reads naturally; no extra label needed
Divider()
.accessibilityHidden(true)
// MARK: Action row
HStack(spacing: 24) {
// Like button with dynamic value
Button {
liked.toggle()
} label: {
Label(
liked ? "Liked" : "Like",
systemImage: liked ? "heart.fill" : "heart"
)
.foregroundStyle(liked ? .red : .secondary)
}
.accessibilityLabel(liked ? "Unlike post" : "Like post")
.accessibilityHint("Double-tap to \(liked ? "remove your like" : "like this post")")
.accessibilityValue("\(likeCount + (liked ? 1 : 0)) likes")
// Share button — icon only, needs a label
Button {
// share action
} label: {
Image(systemName: "square.and.arrow.up")
.foregroundStyle(.secondary)
}
.accessibilityLabel("Share post")
.accessibilityHint("Double-tap to open the share sheet")
// Bookmark button with toggle trait
Button {
// bookmark action
} label: {
Image(systemName: "bookmark")
.foregroundStyle(.secondary)
}
.accessibilityLabel("Bookmark post")
.accessibilityAddTraits(.isButton)
}
.accessibilityElement(children: .contain)
}
.padding()
.background(.background)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.06), radius: 8, y: 4)
}
}
#Preview {
PostCard(
author: "Ada Lovelace",
body: "The Analytical Engine weaves algebraic patterns just as the Jacquard loom weaves flowers and leaves.",
likeCount: 42
)
.padding()
.background(Color(.systemGroupedBackground))
}
How it works
- .accessibilityLabel("Author: \(author)") — overrides the default accessible name that VoiceOver would derive from the view's content. Prefixing with a role word ("Author:") gives users immediate context without them needing to navigate back up the hierarchy.
-
.accessibilityValue("\(likeCount + (liked ? 1 : 0)) likes") — separates the element's identity (label) from its current state (value). VoiceOver announces both: "Like post, 43 likes, button." This pattern mirrors how
StepperandSliderreport their current value. - .accessibilityHint("Double-tap to…") — describes the outcome of the primary gesture in the imperative form recommended by Apple's Human Interface Guidelines. Hints are announced after a brief pause, so keep them concise (under one sentence).
-
.accessibilityHidden(true) on the avatar
ImageandDivider— removes purely decorative or redundant elements from the accessibility tree, keeping the VoiceOver traversal order clean and fast. -
.accessibilityElement(children: .contain) on the action
HStack— keeps each child button as its own focus target while logically grouping them in the tree. Use.combineinstead if you want the whole row to be a single tap target (useful for list rows).
Variants
Combine a card row into one focusable unit
For a settings-style list row where the label and chevron should read as one element, use .accessibilityElement(children: .combine) on the container. VoiceOver merges all child text into a single announcement.
HStack {
Image(systemName: "bell.fill")
.foregroundStyle(.orange)
VStack(alignment: .leading) {
Text("Notifications")
.font(.body)
Text("Enabled")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.tertiary)
}
// VoiceOver says: "Notifications, Enabled"
.accessibilityElement(children: .combine)
.accessibilityHint("Double-tap to open notification settings")
Dynamic labels for loading states
When a view toggles between a loading spinner and real content, update the label dynamically so VoiceOver announces the change. Use a computed property or a ternary inside .accessibilityLabel:
@State private var isLoading = true
ProgressView()
.opacity(isLoading ? 1 : 0)
.accessibilityLabel(isLoading ? "Loading posts" : "Posts loaded")
.accessibilityAddTraits(isLoading ? .updatesFrequently : [])
Common pitfalls
-
iOS version:
.accessibilityLabelhas been available since iOS 13, but.accessibilityDirectTouch(options:)and several newer traits require iOS 17+. Always check the API availability badge in Xcode's documentation inspector before shipping. -
Duplicate announcements: Applying
.accessibilityLabelto aButtonwhoseLabelalready has a text title will not cause double-reading — the modifier overrides the synthesised label entirely. However, if you also add.accessibilityValuewith text that's already in the label, VoiceOver will read it twice. Keep label, value, and hint semantically distinct. -
Performance & layout: Accessibility modifiers are zero-cost at render time, but
.accessibilityElement(children: .combine)causes the framework to walk the entire subtree to merge text. For very deep view hierarchies (e.g., inside aForEachof 1,000 rows) prefer providing an explicit label instead of combining children.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement accessibility labels in SwiftUI for iOS 17+. Use accessibilityLabel, accessibilityHint, accessibilityValue, accessibilityHidden, and accessibilityElement(children:). Make it accessible (VoiceOver labels on every interactive control). Add a #Preview with realistic sample data.
In Soarias's Build phase, paste this prompt into the active session after scaffolding your view — Claude Code will audit every Button, Image, and custom control in the file and add the appropriate accessibility modifiers in one pass.
Related
FAQ
Does this work on iOS 16?
Yes — .accessibilityLabel, .accessibilityHint, .accessibilityValue, and .accessibilityHidden are all available back to iOS 13. The code in this guide compiles and runs on iOS 16 without changes. The iOS 17+ requirement on this page reflects the minimum deployment target recommended by Soarias for new projects in 2026; none of the APIs shown are iOS-17-exclusive.
How do I test VoiceOver labels without a physical device?
The Xcode Accessibility Inspector (Xcode → Open Developer Tool → Accessibility Inspector) lets you point at any running Simulator view and see the exact label, hint, value, and traits that VoiceOver would announce. You can also run the Audit tab to auto-detect missing labels. For on-device testing, triple-click the side button to toggle VoiceOver, then swipe right to step through focusable elements in order.
What's the UIKit equivalent?
In UIKit you set the same concepts as properties directly on UIView: view.accessibilityLabel = "Like post", view.accessibilityHint = "Double-tap to like", view.accessibilityValue = "42 likes", and view.isAccessibilityElement = false for hidden elements. SwiftUI's modifier-based API maps 1-to-1 to these properties under the hood, so bridging between the two in UIViewRepresentable wrappers requires no extra work.
Last reviewed: 2026-05-11 by the Soarias team.