.agents/skills/swiftui-expert-skill/references/accessibility-patterns.md
Prefer Button over onTapGesture for tappable elements. Button provides VoiceOver support, focus handling, and proper traits for free.
System Text scales with Dynamic Type automatically. For custom numeric values (padding, image sizes, spacing), use @ScaledMetric:
struct ProfileHeader: View {
@ScaledMetric private var avatarSize = 60.0
@ScaledMetric private var spacing = 12.0
var body: some View {
HStack(spacing: spacing) {
Image("avatar")
.resizable()
.frame(width: avatarSize, height: avatarSize)
Text("Username")
}
}
}
Specify a relativeTo text style when the value should scale relative to a specific font size:
@ScaledMetric(relativeTo: .caption) private var iconSize = 16.0
Use accessibilityAddTraits and accessibilityRemoveTraits for state-driven traits:
Text(item.title)
.accessibilityAddTraits(item.isSelected ? [.isSelected, .isButton] : .isButton)
Use .disabled(true) to make VoiceOver announce "Dimmed" for non-interactive elements.
HStack {
Image(systemName: "star.fill")
Text("Favorites")
Text("(\(count))")
}
.accessibilityElement(children: .combine)
VoiceOver reads all child labels as one element, separated by commas.
HStack {
Text(item.name)
Spacer()
Text(item.price)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(item.name), \(item.price)")
HStack {
ForEach(tabs) { tab in
TabButton(tab: tab)
}
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Tab bar")
VoiceOver announces the container name when focus enters/exits.
PageControl(selectedIndex: $selectedIndex, pageCount: pageCount)
.accessibilityElement()
.accessibilityValue("Page \(selectedIndex + 1) of \(pageCount)")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
guard selectedIndex < pageCount - 1 else { break }
selectedIndex += 1
case .decrement:
guard selectedIndex > 0 else { break }
selectedIndex -= 1
@unknown default:
break
}
}
When a custom view should behave like a native control for accessibility:
HStack {
Text(label)
Toggle("", isOn: $isOn)
}
.accessibilityRepresentation {
Toggle(label, isOn: $isOn)
}
@Namespace private var ns
HStack {
Text("Volume")
.accessibilityLabeledPair(role: .label, id: "volume", in: ns)
Slider(value: $volume)
.accessibilityLabeledPair(role: .content, id: "volume", in: ns)
}
Button instead of onTapGesture for tappable elements@ScaledMetric for custom values that should scale with Dynamic TypeaccessibilityElement(children:)accessibilityLabel when default labels are unclearaccessibilityRepresentation for custom controlsaccessibilityAdjustableAction for increment/decrement controls