Skip to content

Accessibility Rotors (SwiftUI)

A SwiftUI guide for coding agents. Also covers accessibilityrotor modifier, voiceover rotor navigation, custom rotor ios, rotor entry namespace, navigate filtered items voiceover.

The accessibilityRotor view modifier (SwiftUI Release 3, iOS 15+) builds a custom VoiceOver rotor: a named subset of elements the user flicks through directly. Reach for it when a long list has a category worth jumping between, such as only the negative trends.

Declaring a Rotor

The modifier takes a label (String, LocalizedStringKey, or Text) and an AccessibilityRotorContentBuilder closure. ForEach and Group work inside it, and each AccessibilityRotorEntry binds to a view by ID. SwiftUI matches the entry IDs against the IDs in the list's own ForEach.

struct TrendsView: View {
  let trends: [Trend]

  var body: some View {
    List {
      ForEach(trends, id: \.id) { trend in
        TrendView(trend: trend)
      }
    }
    .accessibilityRotor("Negative trends") {
      ForEach(trends, id: \.id) { trend in
        if !trend.isPositive {
          AccessibilityRotorEntry(trend.message, id: trend.id)
        }
      }
    }
  }
}

Make the row itself one element first, for example accessibilityElement(children: .combine) on the row's HStack, so a rotor stop lands on a sensible unit.

Explicit Binding with a Namespace

When ID matching is not enough, for example when only part of a row belongs in the rotor, bind explicitly: tag the view with accessibilityRotorEntry(id:in:) and a @Namespace, and pass the same namespace to the entry.

@Namespace private var customRotorNamespace

// on the target view
TrendView(trend: trend)
  .accessibilityRotorEntry(id: trend.id, in: customRotorNamespace)

// in the rotor
AccessibilityRotorEntry(trend.message, trend.id, in: customRotorNamespace)

Preparing Entries and Text Ranges

An entry accepts a closure that runs when the user navigates to it. Use it with ScrollViewReader to scroll an off-screen row into view before VoiceOver lands on it.

AccessibilityRotorEntry(trend.message, trend.id, in: customRotorNamespace) {
  scrollView.scrollTo(trend.id)
}

Rotors also work inside text: accessibilityRotor(_:textRanges:) on a TextEditor lets VoiceOver jump between marked ranges, such as emails or links.

TextEditor(text: $content.text)
  .accessibilityRotor("Emails", textRanges: content.emailRanges)
  .accessibilityRotor("Links", textRanges: content.linkRanges)

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-accessibility-rotors" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems