Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/accessibility-patterns.md
1# SwiftUI Accessibility Patterns Reference23## Table of Contents45- [Core Principle](#core-principle)6- [Dynamic Type and @ScaledMetric](#dynamic-type-and-scaledmetric)7- [Accessibility Traits](#accessibility-traits)8- [Decorative Images](#decorative-images)9- [Element Grouping](#element-grouping)10- [Custom Controls](#custom-controls)11- [Summary Checklist](#summary-checklist)1213## Core Principle1415Prefer `Button` over `onTapGesture` for tappable elements. `Button` provides VoiceOver support, focus handling, and proper traits for free.1617## Dynamic Type and @ScaledMetric1819System text styles scale with Dynamic Type automatically. Prefer built-in styles like `.largeTitle`, `.title`, `.title2`, `.title3`, `.headline`, `.subheadline`, `.body`, `.callout`, `.footnote`, `.caption`, and `.caption2` when they fit your UI:2021```swift22VStack(alignment: .leading) {23Text("Inbox")24.font(.title2)25Text("3 unread messages")26.font(.body)27Text("Updated just now")28.font(.caption)29}30```3132For custom fonts, use a Dynamic Type-aware font initializer so the text still follows the user's preferred content size:3334```swift35VStack(alignment: .leading) {36Text("Article")37.font(.custom("SourceSerif4-Semibold", size: 28, relativeTo: .title2))38Text("Body copy")39.font(.custom("SourceSerif4-Regular", size: 17))40}41```4243`Font.custom(_:size:relativeTo:)` lets you match a specific text style. `Font.custom(_:size:)` scales relative to the body style. Avoid fixed-size custom fonts for primary content that should respond to Dynamic Type.4445For non-text numeric values like padding, spacing, and image sizes, use `@ScaledMetric`:4647```swift48struct ProfileHeader: View {49@ScaledMetric private var avatarSize = 60.050@ScaledMetric private var spacing = 12.05152var body: some View {53HStack(spacing: spacing) {54Image("avatar")55.resizable()56.frame(width: avatarSize, height: avatarSize)57Text("Username")58}59}60}61```6263Specify a `relativeTo` text style when the value should track a specific Dynamic Type style, including for images or icons that should stay proportional to nearby text:6465```swift66struct StatusRow: View {67@ScaledMetric(relativeTo: .body) private var iconSize = 18.06869var body: some View {70HStack(spacing: 8) {71Image(systemName: "checkmark.circle.fill")72.font(.system(size: iconSize))73Text("Synced")74.font(.custom("AvenirNext-Regular", size: 17, relativeTo: .body))75}76}77}78```7980## Accessibility Traits8182Use `accessibilityAddTraits` and `accessibilityRemoveTraits` for state-driven traits:8384```swift85Text(item.title)86.accessibilityAddTraits(item.isSelected ? [.isSelected, .isButton] : .isButton)87```8889Use `.disabled(true)` to make VoiceOver announce "Dimmed" for non-interactive elements.9091## Decorative Images9293Use `Image(decorative:bundle:)` when an asset image is purely visual and should not appear in the accessibility tree.9495```swift96Image(decorative: "confetti")97```9899This is appropriate for backgrounds, flourishes, and icons that do not add meaning beyond nearby text.100101If the image conveys information, keep it accessible and provide a clear label:102103```swift104Image("receipt")105.accessibilityLabel("Receipt")106```107108For non-asset images, such as SF Symbols, hide decorative content with `accessibilityHidden(true)` instead:109110```swift111Image(systemName: "sparkles")112.accessibilityHidden(true)113```114115## Element Grouping116117### .combine -- Auto-join child labels118119```swift120HStack {121Image(systemName: "star.fill")122Text("Favorites")123Text("(\(count))")124}125.accessibilityElement(children: .combine)126```127128VoiceOver reads all child labels as one element, separated by commas.129130### .ignore -- Manual label for container131132```swift133HStack {134Text(item.name)135Spacer()136Text(item.price)137}138.accessibilityElement(children: .ignore)139.accessibilityLabel("\(item.name), \(item.price)")140```141142### .contain -- Semantic grouping143144```swift145HStack {146ForEach(tabs) { tab in147TabButton(tab: tab)148}149}150.accessibilityElement(children: .contain)151.accessibilityLabel("Tab bar")152```153154VoiceOver announces the container name when focus enters/exits.155156## Custom Controls157158### Adjustable controls (increment/decrement)159160```swift161PageControl(selectedIndex: $selectedIndex, pageCount: pageCount)162.accessibilityElement()163.accessibilityValue("Page \(selectedIndex + 1) of \(pageCount)")164.accessibilityAdjustableAction { direction in165switch direction {166case .increment:167guard selectedIndex < pageCount - 1 else { break }168selectedIndex += 1169case .decrement:170guard selectedIndex > 0 else { break }171selectedIndex -= 1172@unknown default:173break174}175}176```177178### Representing custom views as native controls179180When a custom view should behave like a native control for accessibility:181182```swift183HStack {184Text(label)185Toggle("", isOn: $isOn)186}187.accessibilityRepresentation {188Toggle(label, isOn: $isOn)189}190```191192### Label-content pairing193194```swift195@Namespace private var ns196197HStack {198Text("Volume")199.accessibilityLabeledPair(role: .label, id: "volume", in: ns)200Slider(value: $volume)201.accessibilityLabeledPair(role: .content, id: "volume", in: ns)202}203```204205## Summary Checklist206207- [ ] Use `Button` instead of `onTapGesture` for tappable elements208- [ ] Use built-in text styles or Dynamic Type-aware custom fonts for text209- [ ] Use `@ScaledMetric` for custom values that should scale with Dynamic Type210- [ ] Mark purely decorative images as decorative or hidden from accessibility211- [ ] Group related elements with `accessibilityElement(children:)`212- [ ] Provide `accessibilityLabel` when default labels are unclear213- [ ] Use `accessibilityRepresentation` for custom controls214- [ ] Use `accessibilityAdjustableAction` for increment/decrement controls215- [ ] Ensure navigation flow is logical when using VoiceOver grouping216