Repository navigation
Description
Steps to reproduce
- Create any scrollable list (
ListView.builderorCustomScrollView+SliverList) with more content than fits one screen. - Run on a physical iOS device, enable VoiceOver.
- Three-finger swipe up/down to scroll.
Expected results
VoiceOver announces the scroll status after the scroll settles, "Page 2 of 5" (or "Rows 6 to 10 of 43"), as it does in every native iOS scroll view (Messages, Settings, Mail). Blind users rely on this to know where they are in a list; without it a paginated feed is disorienting: scrolls happen silently and there is no positional feedback at all.
Actual results
The scroll happens, but nothing is announced. (TalkBack on Android announces correctly.)
Root cause (traced through engine source)
When a VoiceOver scroll action completes, the iOS embedder posts UIAccessibilityPageScrolledNotification with no announcement string — there is a TODO where the string should be, in AccessibilityBridge::UpdateSemantics (engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm):
} else if (scrollOccured) {
// TODO(chunhtai): figure out what string to use for notification. At this
// point, it is guarantee the previous focused object is still in the tree
// so that we don't need to worry about focus lost. (e.g. "Screen 0 of 3")
ios_delegate_->PostAccessibilityNotification(
UIAccessibilityPageScrolledNotification,
FindNextFocusableIfNecessary().nativeAccessibility);
}Per Apple's contract for custom accessibilityScroll: implementations, the app must post UIAccessibilityPageScrolledNotification with a string describing the new scroll position; posting an element yields no speech.
Android doesn't have this problem because the Android embedder emits TYPE_VIEW_SCROLLED events populated with scrollY/maxScrollY/itemCount/fromIndex/toIndex (AccessibilityBridge.java), and TalkBack composes the announcement itself. iOS has no equivalent event schema — the string must be supplied by the app/engine, and the iOS embedder has no localization infrastructure for UI strings, which is presumably why the TODO has remained.
Possible fix directions
Option A — reuse UIKit's native scroll machinery (preferred?). The engine already backs every scrollable semantics node with a real UIScrollView (FlutterSemanticsScrollView) whose contentSize/contentOffset are kept in sync with the Flutter scrollable. It also already supports the OS scrolling that scroll view natively: SemanticsAction.kScrollToOffset forwards a system-driven setContentOffset back to the framework (added for UIFocusItemScrollableContainer support, see SemanticsObject+UIFocusSystem.mm). If accessibilityScroll: let UIKit's built-in UIScrollView accessibility scrolling run (with isDoingSystemScrolling set) instead of dispatching kScrollUp/kScrollDown to Dart, UIKit would page the scroll view and produce its own system-localized scroll-status announcement — no engine localization needed. This might also be a path toward exposing the native VoiceOver "vertical scroll bar" element, i.e. #79355. Open questions: UIKit pages by full viewport vs the framework's 0.8-viewport convention (RenderSemanticsGestureHandler.scrollFactor); interaction with infinite/estimated extents (the engine already substitutes a large finite contentSize for infinite scrollExtentMax); the engine's manual PageScrolledNotification post would need suppressing on this path.
Option B — implement the TODO literally. Compute the string at the posting site from the scrolled node's scrollPosition/scrollExtentMin/scrollExtentMax and viewport rect (or scrollChildren/scrollIndex for "rows x to y of z"). Blocker: needs localized strings in the engine, or plumbing a localized template from the framework through the semantics update.
Context / impact
I build an audio app with a substantial blind user base. Multiple VoiceOver users have independently reported this as their top navigation obstacle, explicitly comparing it to the "Page 1 of 27" behavior they get in native iOS apps.
Related but distinct: #79355 tracks the missing VoiceOver "vertical scroll bar" element; #164483 (fixed by #184155) covered the silent focusable element before scroll views.
Flutter version
Reproduced on Flutter 3.44.5 (stable) / iOS 18–26 devices; the relevant engine code is unchanged on master as of this filing.
Activity
mbcorona commentedon Jul 11, 2026
Thanks for the detailed report and source analysis.
Reproduced on an iPhone 15 Pro Max with VoiceOver: a three-finger scroll on a plain
ListView.builderscrolls the list but announces no position — no "Page X of Y" / "Rows X to Y". In my repro (list under anAppBar) focus also landed on the header after the scroll and VoiceOver read that instead, which lines up with the notification posting the next focusable element rather than a scroll-status string.The
scrollOccuredbranch inaccessibility_bridge.mmstill has the TODO in place of the position string on master.Routing to the accessibility team. #79355 and #164483 look separate from this.
zywek123 commentedon Jul 23, 2026
@davefaliskie issues related, not fixed yet
#22747 #64279 #64924 #98572 #141607
Each of them concerns the same issue: the lack of notifications when scrolling with three fingers.
haashem commentedon Sep 11, 2026
flutter.MP4
ios.MP4
Sample app:
chunhtai commentedon Sep 14, 2026
not entirely sure how to properly handle this, but here is some options
figure out if we can implement this API somehow https://developer.apple.com/documentation/uikit/uiscrollviewaccessibilitydelegate?language=objc
If 1 doesn't work, we can provide our own custom string in
flutter/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm
Line 234 in 8225c75
[iOS] Announce scroll position to VoiceOver
[iOS] Announce scroll position to VoiceOver
[iOS] Announce scroll position to VoiceOver