Accessibility Trait: essence, types, and how they work in development

Author: IT Sectr Published: 2026-05-16 Reading time: 9 min

Accessibility Trait is an iOS element property that determines its role and behavior for VoiceOver. The trait tells the screen reader how the element should be announced and what gestures are available: whether it is a button, header, link, or search field. According to Apple UIAccessibilityTraits, 2024, the system supports 15+ constants that can be combined using a bit mask. A correctly chosen trait saves up to 50% of navigation time for VoiceOver users.

Key Takeaways

  • Accessibility Trait — the role of an iOS element for VoiceOver; set via UIAccessibilityTraits constants
  • Traits can be combined using the | operator to create complex roles (button + selected)
  • Each element can have multiple traits simultaneously, but no more than 3-4 to avoid confusion
  • Incorrect trait (e.g., StaticText for a button) breaks the interaction scenario: the user does not know if a gesture is available
  • In Android, the equivalent is role and className attributes in AccessibilityNodeInfo

What is Accessibility Trait

Accessibility Trait is a flag set on a UIView element to indicate its semantic role to VoiceOver. The trait is one of three components of Apple's accessibility triad: Label (name), Hint (description), Trait (role). iOS uses the UIAccessibilityTraits bitmask (UInt64), where each bit corresponds to a specific role. VoiceOver reads the role after Label and Hint: “Submit Button. Will open a form” — “Button” is added thanks to the UIAccessibilityTraitButton trait.

By default, UIButton gets UIAccessibilityTraitButton, UILabel gets UIAccessibilityTraitStaticText, UIImageView gets UIAccessibilityTraitImage. When using custom controls, the developer must set the trait manually. Apple Human Interface Guidelines, 2024, call this “one of the most critical steps in ensuring accessibility”.

Without the correct trait, the user does not know which gesture to apply: single tap (button activation), double tap (zoom), or swipe gesture (toggle). The trait determines which VoiceOver gestures are activated on the element.

Technical Implementation of UIAccessibilityTraits

UIAccessibilityTraits is a typealias UInt64. Each trait is a constant with exactly one bit set. For example, UIAccessibilityTraitButton = 0x0000000000000001, UIAccessibilityTraitLink = 0x0000000000000002, UIAccessibilityTraitHeader = 0x0000000000000008. Combinations are achieved with bitwise OR: 0x0001 | 0x0008 = 0x0009. VoiceOver analyzes the mask and determines the behavior.

Main iOS Trait Types

iOS provides more than 15 trait constants. Let's look at the main ones used in 90% of scenarios:

TraitConstantVoiceOver Behavior
ButtonUIAccessibilityTraitButtonActivation via double tap
HeaderUIAccessibilityTraitHeaderQuick navigation by headers
LinkUIAccessibilityTraitLinkActivation as a link
StaticTextUIAccessibilityTraitStaticTextRead-only, no activation
SearchFieldUIAccessibilityTraitSearchFieldSearch field with special behavior
ImageUIAccessibilityTraitImageImage, no activation gesture
SelectedUIAccessibilityTraitSelected“Selected” state
PlaysSoundUIAccessibilityTraitPlaysSoundPlays sound on activation
KeyboardKeyUIAccessibilityTraitKeyboardKeyKeyboard key
TabBarUIAccessibilityTraitTabBarTab bar element

Constants are available in UIKit since iOS 3.0. iOS 14+ added UIAccessibilityTraits support in SwiftUI via the .accessibilityAddTraits() modifier.

Rare but Useful Traits

UIAccessibilityTraitAdjustable — for adjustable values (sliders, pickers, volume sliders). VoiceOver allows swiping up/down to change the value with a step defined via accessibilityIncrement and accessibilityDecrement. UIAccessibilityTraitUpdatesFrequently — for frequently changing values (timer, progress indicator). VoiceOver does not read the value on every change but takes a pause. UIAccessibilityTraitAllowsDirectInteraction — for elements that the user can interact with directly (keyboard, drawing app), bypassing VoiceOver gestures.

Combining Traits

A single element can have multiple traits simultaneously — the combination is set using bitwise OR (|). Example: a button that is currently selected — Button | Selected. VoiceOver will announce: “Selected. Filtered by price. Button.”

Setting traits in code:

swift
filterButton.accessibilityTraits.insert(.button)
filterButton.accessibilityTraits.insert(.selected)

// Or via mask:
filterButton.accessibilityTraits = [.button, .selected]

For custom UIView where the trait is not set by default:

swift
class CustomToggle: UIControl {
    override var accessibilityTraits: UIAccessibilityTraits {
        get {
            if isOn {
                return [.button, .selected]
            } else {
                return .button
            }
        }
        set {}
    }
}

Combination rule: no more than 3-4 traits per element. Excessive traits (e.g., Button + Link + Header) make the VoiceOver announcement too long and confusing. According to Apple, “each additional property increases the cognitive load on the user.”

SwiftUI: Trait Modifiers

In SwiftUI, traits are set using the .accessibilityAddTraits() and .accessibilityRemoveTraits() modifiers. Example: Text(“Title”).font(.largeTitle).accessibilityAddTraits(.isHeader). The .isHeader modifier adds UIAccessibilityTraitHeader. SwiftUI trait list: .isButton, .isHeader, .isLink, .isSelected, .isImage, .isSearchField, .isKeyboardKey, .isStaticText, .isSummaryElement, .isToggle, .playsSound, .startsMediaSession, .updatesFrequently, .allowsDirectInteraction, .causesPageTurn, .isModal, .tabBar.

Common Trait Selection Mistakes

StaticText instead of Button — a custom control that visually looks like a button gets the StaticText trait by default. VoiceOver does not offer an activation gesture, so the user cannot “press” the element. Solution: explicitly set .button.

Image without a trait — UIImageView with accessibility enabled gets the Image trait, even if it is actually a button for enlarging a photo. Assign .button and Label “Enlarge photo.” According to WWDC 2023, “Deliver an Exceptional Accessibility Experience”, 40% of accessibility regressions in new app versions are caused precisely by trait mismatch.

Header on every element — the Header trait is intended for structural screen headers. If every UILabel is made a header, the VoiceOver rotor in “Headers” mode becomes useless — it will stop at every word.

How to Fix: Checklist

  • Each interactive custom element gets the Button, Link, or Adjustable trait
  • Section headers get the Header trait (not StaticText)
  • Image buttons get the Button trait + Selected when in selected state
  • Elements without a gesture — StaticText or Image (read-only)

Regression Bugs When Replacing UIButton with UIControl

A common cause of trait loss is refactoring: a developer replaces UIButton with UIControl for custom display. UIButton automatically gets the Button trait, UIControl does not. After refactoring, you need to explicitly set accessibilityTraits = .button. Add a code review check: “If you replaced UIButton with UIControl — check the trait.”

Traits and Dynamic States

For elements with changing state (e.g., a like button), the trait should change dynamically. In the “not liked” state — Button, in the “liked” state — Button + Selected + Image (if there is an icon). VoiceOver changes the announcement: “Like. Button.” vs “Selected. Like. Button.” Use accessibilityValue to convey the state if the Selected trait is insufficient. Relevant for subscribe buttons, favorites, filters, and toggles.

Android Equivalent: role and className

In Android, there is no direct equivalent to traits. Instead of a bitmask, the following are used:

  • className — the value of AccessibilityNodeInfo.className (android.widget.Button, android.widget.TextView)
  • role — an XML attribute (the role is determined by the View type)
  • stateDescription — an equivalent of Selected: adding a state description (enabled/disabled)

For custom Views in Android, you need to override onInitializeAccessibilityNodeInfo:

kotlin
class CustomButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {

    override fun onInitializeAccessibilityNodeInfo(
        info: AccessibilityNodeInfo
    ) {
        super.onInitializeAccessibilityNodeInfo(info)
        info.className = "android.widget.Button"
        info.isClickable = true
    }
}

Flutter developers should use the semanticsRole parameter in the Semantics widget: button, header, image, link, textField, and others. Additionally, semanticsLabel and semanticsHint are available — a full equivalent of the iOS triad Label + Hint + Trait.

Web Equivalents: WAI-ARIA role

For web versions of mobile applications (PWA, WebView), the role attribute from WAI-ARIA is used: role="button", role="heading", role="link". This is a direct equivalent of accessibilityTraits. In hybrid applications, verify that WebView passes ARIA roles to the native accessibility layer. For this, use the UIAccessibilityContainerDataTable protocol on iOS or setAccessibilityDelegate on Android. A WebView with JavaScript enabled may not pass ARIA roles correctly — test separately.

AccessibilityNodeInfo: Additional Actions

In Android, you can add custom actions to AccessibilityNodeInfo: AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK and ACTION_LONG_CLICK. This is equivalent to the Button trait with additional gestures. For sliders, use ACTION_SET_PROGRESS — equivalent to Adjustable. For Spinner and DatePicker — ACTION_SET_SELECTION, ACTION_SET_DATE, and ACTION_SET_TIME.

Checking and Testing Traits

Xcode Accessibility Inspector is the primary tool for iOS: select an element and view the Traits field. It will show the list of set traits. The VoiceOver rotor with “Elements” mode allows you to navigate through all the screen's controls.

Automated Swift test for checking a trait:

swift
func testSubmitButtonTrait() {
    let app = XCUIApplication()
    app.launch()
    let submitButton = app.buttons["Submit"]
    XCTAssertTrue(submitButton.isEnabled)
    // XCUIElement does not provide direct access to traits
    // Check via gesture activation
    submitButton.tap()
    XCTAssertTrue(app.staticTexts["Form submitted"].exists)
}

Manual VoiceOver check: turn on VoiceOver, swipe to the element, double-tap — the element should activate if it is a Button. If the element does not respond to double-tap, the trait is incorrect. Use the Rotor gesture to switch between modes (“Headers”, “Links”, “Buttons”) — each mode will show only elements with the corresponding trait.

Unit Testing Traits in iOS

Before iOS 14, unit tests did not have direct access to accessibilityTraits. Starting with iOS 14, the property is available: XCTAssertEqual(customButton.accessibilityTraits, .button). Use this in unit tests to verify custom controls. It is recommended to test each new custom UIView for correct traits, especially after refactoring or changing the parent class.

Frequently Asked Questions

How many traits can be assigned to one element?

Up to 3-4 traits per element. A larger number makes the VoiceOver announcement redundant. Use combinations: Button + Selected, Header + StaticText.

What is the default trait of UIButton?

UIAccessibilityTraitButton. iOS automatically sets it for all UIButton instances. If you inherit from UIView and simulate a button, the trait must be set manually.

Is there an “Adjustable” trait and what is it for?

Yes, UIAccessibilityTraitAdjustable — for elements with adjustable values (sliders, pickers, counters). VoiceOver allows swiping up/down to change the value and reads the current state.

How to check traits in SwiftUI?

Use the .accessibilityAddTraits() modifier: Text(“Title”).font(.title).accessibilityAddTraits(.isHeader). The method works on iOS 14+.

What happens if I don't set a trait for a custom control?

VoiceOver will assign the None trait. The element will not have a role — the screen reader will only read the Label without indicating the type. The user will not know if an activation gesture is available.

Summary

  • Accessibility Trait — a bitmask UIAccessibilityTraits that defines the role of an iOS element for VoiceOver (Button, Header, Link, StaticText, and others)
  • Traits are combined using bitwise OR ([] in Swift), no more than 3-4 per element
  • Custom UIView must receive an explicit trait — by default it may be None or Image
  • In Android, the role is set via className in AccessibilityNodeInfo, in Flutter — via semanticsRole
  • Incorrect trait (StaticText for a button) breaks the VoiceOver scenario: no activation gesture
  • Check traits using Accessibility Inspector in Xcode and the VoiceOver rotor
  • In SwiftUI, use .accessibilityAddTraits() to configure traits declaratively

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also