Highlight
Accessibility on Mac differs from iPhone and iPad in one key way: Mac interfaces are denser, and nested containers run deeper. VoiceOver navigates by container on Mac by default, which means a poorly structured accessibility element tree forces users to drill into and pop out of containers at every step. The experience becomes painful fast.
Core Content
Picture this: a user opens your Mac text editor and wants to flip the “Bold” switch in the Format Inspector on the right. A sighted user glances over, moves the mouse, and is done. A VoiceOver user has to press the arrow keys through 22 accessibility elements one by one — Title, Apply, Subtitle, Apply, Heading, Apply — before reaching “Bold.” Nicholas plays the actual audio in the session, and just hearing it is enough to make your skin crawl.
The defining traits of the Mac are keyboard and mouse input, dense information, and many windows running at once. SwiftUI treats every view as its own accessibility element by default, and VoiceOver on Mac jumps by container by default. Together, these push apps into one of two extremes: either the tree is so flat that every step plows through every element, or it nests so deep that users keep focusing in and focusing out of containers. This year Apple gave us three tools to fix it: reshape the layout with accessibilityElement(children:), speed up navigation with rotors and accessibilityDefaultFocus, and back hover-only actions with a keyboard equivalent through accessibilityAction. The session walks a text-editor demo through all three, step by step.
Detailed Content
Step 1: reshape the layout with accessibilityElement(children:) (04:15). The modifier offers three behaviors: contain groups child views into a container, combine merges children into a single element, and ignore hides the children entirely. In the demo, the 22 style preset elements collapse into one group named “Style Presets”:
// Contain style presets in accessibility container
struct FormattingInspectorView: View {
var body: some View {
Form {
VStack {
StylePresetView(type: .title)
StylePresetView(type: .heading)
StylePresetView(type: .subHeading)
StylePresetView(type: .body)
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Style Presets")
}
}
}
Key points:
.accessibilityElement(children: .contain): turns the VStack into a container. The children remain independent elements, but VoiceOver skips over the outer wrapper by default..accessibilityLabel("Style Presets"): gives the container a clear name. The user hears “Style Presets, Group” and knows what is inside.- After the change, the total element count drops from 22 to 15, and the “Bold” switch is just a few arrow keys away.
Step 2: merge paired elements with combine (06:21). Each style preset is HStack { Title, Button }. VoiceOver reads “Title” and “Apply - button” separately, twice. Use .combine to fuse them into one element:
struct StylePresetView: View {
let preset: StylePreset
var body: some View {
HStack {
PresetTitleView(preset: preset)
Button("Apply") { /* ... */ }
}
.accessibilityElement(children: .combine)
}
}
Key points:
.combinemerges the children’s labels, traits, and actions into a single element.- VoiceOver reads “Title, Apply, button” in one pass, and the action’s meaning is clear.
- The element count is cut in half again, from 8 to 4.
Step 3: turn “find a bookmark” into a one-key jump with rotors (08:43). A sighted user scans the sidebar and instantly sees which pages are bookmarked. A VoiceOver user has to listen page by page. A rotor lets the user pop up a menu with one keyboard shortcut, listing only the elements that match:
struct PagesView: View {
@Binding var pages: [Page]
var body: some View {
List(pages) { page in
PageListItemView(page: page)
}
.accessibilityRotor("Bookmarks") {
ForEach(pages) { page in
if page.isBookmarked {
AccessibilityRotorEntry(page.title, id: page.id)
}
}
}
}
}
Key points:
.accessibilityRotor("Bookmarks"): registers a rotor named “Bookmarks.”AccessibilityRotorEntry(page.title, id: page.id): each entry maps to a jump target. Theidmust locate the matching row inside the List.- Once VoiceOver focuses on the List, the rotor shortcut opens the menu and jumps straight to “Page 5 bookmarked.”
Step 4: suggest the initial focus with accessibilityDefaultFocus (09:33). New in macOS / iOS 26, this modifier lets the app suggest where VoiceOver should land when a new scene appears:
struct MyView: View {
@AccessibilityFocusState(for: .voiceOver) var focusedForVoiceOver
var body: some View {
FirstView()
SecondView()
.accessibilityDefaultFocus($focusedForVoiceOver, true)
ThirdView()
}
}
Key points:
@AccessibilityFocusState(for: .voiceOver): a focus state scoped to VoiceOver only. It does not affect keyboard focus..accessibilityDefaultFocus($focusedForVoiceOver, true): when the scene appears, suggest that SecondView take focus.- It is only a suggestion. VoiceOver may pick a different target based on user preferences, so the app should not assume otherwise.
Step 5: back hover-only actions with accessibilityAction (10:28). The bookmark button in the demo only appears when the mouse hovers over a page thumbnail. VoiceOver users have no hover gesture, so the button stays out of reach:
struct PageListItemView: View {
var page: Page
var body: some View {
VStack() {
ThumbnailView(page: page)
Text(page.title)
}
.onHover { /* ... */ }
.accessibilityAction(named: page.isBookmarked ? "Remove Bookmark" : "Bookmark") {
page.isBookmarked.toggle()
}
}
}
Key points:
.accessibilityAction(named:): declares a named action that VoiceOver, Voice Control, and Switch Control can all invoke.- The name shifts with the current state (“Bookmark” / “Remove Bookmark”), so the user knows what comes next.
- The action does not depend on mouse hover. Keyboards and assistive technologies reach it directly.
Core Takeaways
-
What to do: run VoiceOver as a regression test early in the project.
- Why it matters: Nicholas hammers home the point of “test from day one.” The earlier a problem surfaces, the cheaper it is to fix. Listening to VoiceOver during the visual layout stage catches structural issues — too many elements, wrong order, deep container nesting — right away.
- How to start: open VoiceOver on macOS with
Cmd + F5, then walk through a freshly written view withCtrl + Option + arrow keys. Make “how many key presses to get through one full page” a metric the team can watch.
-
What to do: layer dense interfaces with
accessibilityElement(children:).- Why it matters: Mac inspectors, sidebars, and toolbars usually pack in many parallel controls. A flat tree forces VoiceOver users through dozens of key presses. A few lines of
containplusaccessibilityLabelcut the element count in half. - How to start: apply
.containto the outermost group with a meaningful.accessibilityLabel, then move inward and use.combineon semantically paired children (Title + Button, Icon + Label). Keep nesting to three levels or fewer.
- Why it matters: Mac inspectors, sidebars, and toolbars usually pack in many parallel controls. A flat tree forces VoiceOver users through dozens of key presses. A few lines of
-
What to do: add a rotor for important collections.
- Why it matters: bookmarks, errors, to-dos, unread messages — the “small subsets scattered through long lists” — are where VoiceOver users get lost most easily. A rotor turns an O(N) scan into an O(1) menu jump.
- How to start: pick the element types users most often want to jump straight to, then add
.accessibilityRotor("name") { ForEach { AccessibilityRotorEntry(...) } }to the matching List or ScrollView. Test with VoiceOver to confirm the shortcut menu lands on the right row.
-
What to do: audit every hover-only and gesture action, and back it with
accessibilityAction.- Why it matters: many Mac buttons that “only appear on hover” are invisible controls to VoiceOver, Switch Control, and Voice Control users. A single
.accessibilityAction(named:)opens the action up to every assistive technology. - How to start: grep the project for
.onHoverand.gesturecall sites, and check whether each one triggers a state change or navigation. If it does, add.accessibilityAction(named:)on the same view, with the name written as a verb phrase.
- Why it matters: many Mac buttons that “only appear on hover” are invisible controls to VoiceOver, Switch Control, and Voice Control users. A single
Related Sessions
- Customize your app for Assistive Access — tailor the app for Assistive Access mode so users with cognitive disabilities can pick it up
- Evaluate your app for Accessibility Nutrition Labels — the new App Store accessibility nutrition labels: self-evaluate before you ship
- Principles of inclusive app design — start from understanding disability and rebuild app design principles
- Catch up on accessibility in SwiftUI — the prerequisite session that explains the basic model of accessibility elements
Comments
GitHub Issues · utterances