Preparing your App for VoiceOver: Headers and Heading Level

Preparing your App for VoiceOver: Headers and Heading Level

Learn how to properly use headers and heading levels to enhance the accessibility of your application.

Headers play a crucial role in visually organizing information within the user interface. They act as guides, helping users navigate content more effectively by quickly identifying different sections. This enhances the browsing experience and allows users to focus on the most relevant or interesting parts of the interface.

0:00
/0:17

Navigating App Store sections through headers using VoiceOver

For VoiceOver users headers are fundamental as they facilitate seamless navigation with vertical swipes. For example, one useful option in the VoiceOver rotor is the ability to navigate through headings by swiping up to move to the previous header and swiping down to jump directly to the next one.

0:00
/0:27

Navigating Music app sections through headers using VoiceOver

In both UIKit and SwiftUI, we can mark elements of the user interface as headers by applying the header trait. Traits are used to define how assistive technologies recognize an element.

UIKit

In UIKit, marking a label as a header is straightforward. You can achieve this by adding the .header trait to an element with the .accessibilityTraits.insert() method. This simple addition ensures that VoiceOver recognizes it as a header.

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        label.accessibilityTraits.insert(.header)
    }
}

SwiftUI

In SwiftUI we can create a more structured and organized heading hierarchy. Using the accessibilityAddTraits(_:) modifier we can mark an element as a header with the predefined isHeader trait value.

Additionally, we can specify the heading level of the element ranging from level 1 to level 6 by using the accessibilityHeading(_:) modifier.

import SwiftUI

struct HeadingsView: View {
	var body: some View {
		VStack{
			Text("Hello Accessibility")
    			.accessibilityAddTraits(.isHeader)
                .accessibilityHeading(.h1)
      }
   }
}

The .accessibilityHeading(_:) modifier takes as input an object of type AccessibilityHeadingLevel. When you do not need to establish a heading hierarchy you can pass the .unspecified parameter to the modifier.

In the example below you can see how hierarchy was defined in the content:

import SwiftUI

struct HeadingsView: View {

    var body: some View {
        NavigationStack{
            VStack(alignment: .leading){
                
                /// H2 Header
                Text("A simple list of elements")
                    .accessibilityAddTraits(.isHeader)
                    .accessibilityHeading(.h2)
                    .padding()
                
                /// H3 Header for each section
                List {
                    Section(header: Text("Animals")) {
                        Text("Dog")
                        Text("Cat")
                        Text("Bird")
                    }
                    .accessibilityHeading(.h3)
                    
                    Section(header: Text("Fruits")) {
                        Text("Apple")
                        Text("Banana")
                        Text("Orange")
                    }
                    .accessibilityHeading(.h3)
                    
                    Section(header: Text("Colors")) {
                        Text("Red")
                        Text("Green")
                        Text("Blue")
                    }
                    .accessibilityHeading(.h3)
                }
                .listStyle(GroupedListStyle())
                
                /// H1 Header
                .navigationTitle("Headings")
                
            }
        }
    }
    
}

In this case, we are using the navigationTitle(_:) modifier that already is recognized as a primary header by the system and will be the first element highlighted by VoiceOver.

The text below is marked as a secondary heading using the combination of the two modifiers.

Then we have a list composed of three sections. Having the heading level defined allows users to directly access the desired section, such as "Colors", through vertical swipes without having to go through the inner elements of the previous sections.

This seamless navigation is facilitated by the Section view's built-in header trait but to enhance clarity and comprehension of the content, we simply added the appropriate heading levels.

When to use Header

You should use at least one header on each screen of your app making it the first element highlighted by VoiceOver. This will provide your users with enough information to orient themselves. By implementing headers to mark each different section of an app users can easily find the content they are searching for.

Some elements, such as navigationTitle and sections within lists, come with a built-in header trait, so you don’t need to explicitly mark them as headers.

When to not use Header

Adding too many headers can overwhelm users. It's crucial to ensure that each header should have a clear role, helping users navigate easily through the app.

By keeping things simple, we can ensure users have a smooth and intuitive experience without feeling overwhelmed by too much information at once.

Conclusion

By using the header trait, we ensure that VoiceOver recognizes the element as a header while providing additional context about its hierarchical importance within the content structure.

This level of detail enhances accessibility and enables VoiceOver users to navigate through the interface with greater precision and understanding without endless swipes.