Mobile Accessibility Bible
DAC Accessibility Specialist
iOSinput-navigationBeginner

Designing Accessible Navigation and Focus Management

Good navigation and focus management are critical for accessibility, as they help screen readers and other assistive technologies announce content in a clear, predictable order.

🎯 Why It Matters

Good navigation and focus management are essential for creating an accessible and intuitive user experience. They help screen readers and other assistive technologies announce content in a clear, predictable order, reducing confusion and cognitive load. When implemented properly, focus transitions ensure users with visual or motor impairments can efficiently explore content without losing their place.

đź’» Implementation

Correct Code Example: Programmatic Focus for a Seamless VoiceOver Experience

Demo Description

In this example, when transitioning between quiz questions or form sections, you can automatically update VoiceOver focus so it reads new content immediately.

Implementation

// SwiftUI   
// ACCESSIBLE: Automatically refocuses on new quiz questions so VoiceOver reads updated content.
  
@AccessibilityFocusState private var questionFocused: Bool
  
VStack(spacing: 16) {   
    Text("Question \(currentQuestionIndex + 1) / \(totalQuestions)")   
        .accessibilityLabel("Question \(currentQuestionIndex + 1) of \(totalQuestions)")   
  
    Text(quizQuestions[currentQuestionIndex].question)   
}   
.accessibilityElement(children: .combine)   
.accessibilityLabel(   
    "Question \(currentQuestionIndex + 1) of \(totalQuestions). " +   
    quizQuestions[currentQuestionIndex].question   
)   
.accessibilityFocused($questionFocused)   
  
// After user taps "Next", re-focus the updated question: 
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {   
    questionFocused = false   
    questionFocused = true   
}

Code Explanation

In this snippet, the at-Accessibility-Focus-State property wrapper and the accessibility-Focused function, parsing the focus state, work together to ensure VoiceOver focuses on the updated quiz question. Each time the user advances, the system first unfocuses, then refocuses the content, prompting VoiceOver to announce it as if it were a new element.

Correct Code Example: Creating Accessible Tab-Based Navigation in SwiftUI

Demo Description

Here’s a code snippet demonstrating how to build an accessible tab bar using TabView, which makes top-level sections of your app easily discoverable and navigable with VoiceOver and other assistive technologies:

Implementation

// SwiftUI
// ACCESSIBLE: Each tab is clearly labelled and discoverable by VoiceOver.

struct MainTabView: View {
    var body: some View {
        TabView {
            ContentView()
                .tabItem {
                    Label("Home", systemImage: "house")
                }
            QuizView()
                .tabItem {
                    Label("Quiz", systemImage: "questionmark.text.page")
                }
            MoreView()
                .tabItem {
                    Label("About", systemImage: "info.bubble.fill.rtl")
                }
        }
    }
}

Code Explanation

In this snippet, TabView provides a fully accessible tab bar where each tabItem uses a Label with both text and an icon. VoiceOver automatically announces the tab labels — “Home,” “Quiz,” and “About” — enabling users to easily understand and navigate the app’s main sections. Within a tab, a Navigation Stack—often shortened to “nav stack”—manages deeper routes such as Home › Product List › Details. Users first choose a section through the tab bar and then explore pages inside that section through the stack. In other words, extra navigation options become available after the user taps a tab.

Correct Code Example: Navigation Stack with Clear Screen Titles

Demo Description

A navigation bar stays fixed to the top of the screen and provides a title describing the current screen, a built in Back affordance that matches iOS conventions, and optional leading or trailing buttons for primary actions.

Implementation

// SwiftUI
// ACCESSIBLE: Uses standard navigation patterns recognised by VoiceOver.

NavigationStack {
    List {
        NavigationLink("Item One", destination: DetailView())
        NavigationLink("Item Two", destination: DetailView())
    }
    .navigationTitle("Items")
}

Code Explanation

In this example, NavigationStack creates a navigation context, enabling hierarchical navigation between views. Each NavigationLink represents a tappable row that leads to a new screen—in this case, a DetailView. The .navigationTitle("Items") modifier sets a clear, screen-reader-friendly title that appears in the navigation bar, giving users immediate context. Because navigation bars are standard components, VoiceOver instantly recognises the Back button, announces the title and exposes any toolbar actions without extra work from you

Correct Code Example: Accessible Search Field

Demo Description

Search functionality is a key accessibility and usability feature when dealing with long lists or large collections. By allowing users to quickly locate specific items, a search field reduces cognitive load and physical effort—especially important for people who use assistive technologies or have limited dexterity.

Implementation

// SwiftUI
// ACCESSIBLE: Search field includes placeholder and clear button for screen reader users.

struct SearchableView: View {
    @State private var searchText = ""
 
    var body: some View {
        List {
            // Filtered content based on searchText
        }
        .searchable(text: $searchText, prompt: "Search items")
    }
}

Code Explanation

SwiftUI’s .searchable() modifier allows you to embed a search bar in the navigation bar. This built-in approach includes placeholder text, suggestions, and a Clear button, all of which help screen reader users by providing context for the search and letting them reset the field easily. Apple encourages concise, meaningful placeholder text to clarify what can be searched, along with offering recent or popular suggestions to reduce typing.

âś… Best Practices

Do's

  • âś“Use `@AccessibilityFocusState` to programmatically manage VoiceOver focus
  • âś“Keep navigation structures consistent and predictable
  • âś“Provide descriptive labels for tab and navigation items
  • âś“Use built-in SwiftUI components (`TabView`, `NavigationStack`, `searchable()`) for automatic accessibility

Don'ts

  • âś—Manually manipulate focus order without user context
  • âś—Use custom navigation elements that don’t expose accessibility traits
  • âś—Omit labels or rely on icons alone for navigation cues
  • âś—Forget to delay focus changes when content or layout updates (use a short async delay)

🔍 Common Pitfalls

Losing Focus on New Screens

Not resetting focus causes VoiceOver to stay on hidden elements

Unlabelled Navigation Items

Icons without text confuse screen reader users

Custom Tab Bars

Recreating system tab bars often breaks accessibility

Delayed Announcements

Forgetting to delay focus reset (e.g., with `DispatchQueue.main.asyncAfter`)

VoiceOver

Announces focused elements, tab names, and screen titles automatically

Switch Control

Recognises each tab and navigation link as separate actionable elements

Dynamic Type

Tab labels and navigation titles scale automatically with user font settings

đź§Ş Testing Steps

  1. 1Enable VoiceOver: Go to Settings → Accessibility → VoiceOver
  2. 2Navigate Tabs: Swipe left/right through tab bar items
  3. 3Change Screens: Move between quiz questions or form sections
  4. 4Use Search: Test if VoiceOver correctly announces the search field and results
  5. 5Rotate Device: Ensure reading order remains logical in landscape