Repository navigation
Description
Steps to reproduce
-
Build and run any app containing a
Scrollable(ListView,SingleChildScrollView, etc.) with more content than fits in one screen. See minimal repro below (confirmed). -
On a physical iPhone running iOS 27.0, turn on VoiceOver (Settings > Accessibility > VoiceOver).
-
Swipe right with one finger repeatedly to move VoiceOver's accessibility focus through the on-screen elements, until you reach the last element that is currently visible on screen.
-
Continue swiping right with one finger.
Expected results
VoiceOver's focus advances to the next element, which is currently off-screen. Flutter's hasImplicitScrolling semantics action fires and the Scrollable scrolls automatically to bring that element into view, exactly as it does on iOS 26 and earlier.
This is standard behaviour we rely on throughout our app (several list-based screens) and it worked correctly
before the iOS 27 update.
Actual results
Nothing happens. The single-finger "next element" swipe does not move focus past the last visible element, and the screen does not scroll. The only way to make further progress is a 3-finger swipe (VoiceOver's manual page-scroll gesture), which does scroll the view; after which single-finger swiping resumes working normally within the newly-revealed area, until the next boundary, where the same thing happens again.
This reproduces:
- On a fresh, minimal Flutter project (see Code sample below) - a stock
ListView.builder, no app-specific code involved at all. - On a real production app too (multiple screens: a
SingleChildScrollView+Columnlayout, and separately a plainListViewboth affected identically, so this isn't specific to oneScrollablevariant). - On both Flutter 3.44.0 (stable, released 2026-05-15) and Flutter 3.47.4 (stable, released 2026-09-10) ruling out "engine hasn't caught up with iOS 27 yet" as of that build.
- Only on iOS 27.0 (24A437). The same app on iOS 26.6.2, and other (non-Flutter) apps on the same iOS 27.0 device (e.g. Settings, Messages), do not show this problem; VoiceOver's implicit scrolling works normally there.
So this looks like a regression specific to how iOS 27 drives Flutter's accessibility bridge for scroll actions, not a general system-wide VoiceOver bug, and not an issue with our app's semantics tree (dumped via ext.flutter.debugDumpSemanticsTreeInTraversalOrder - the affected Scrollable's semantics node carries the correct hasImplicitScrolling flag and scrollToOffset/scrollUp actions, matching ScrollPosition._updateSemanticActions() exactly).
We also tried working around this at the app level, adding onDidGainAccessibilityFocus handlers that call Scrollable.ensureVisible() on the theory that focus was reaching the hidden node but the view wasn't following. That didn't help: a single-finger swipe never gets far enough to focus the hidden node in the first place, so the handler never fires. This points at the OS/engine layer that decides whether to invoke the scroll action in response to the swipe gesture, not at anything app-code can influence.
Code sample
Confirmed reproducing on-device against this exact minimal project (Flutter 3.44.0, iOS 27.0), single-finger swipe stalls at the same point a plain ListView scrolls, identically to the production app:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('VoiceOver implicit scroll repro')),
body: ListView.builder(
itemCount: 50,
itemBuilder: (context, index) => ListTile(
title: Text('Item $index'),
),
),
),
);
}
}Screenshots or Video
Logs
Semantics tree dump (relevant excerpt, `ext.flutter.debugDumpSemanticsTreeInTraversalOrder`)
SemanticsNode#75
Rect.fromLTRB(0.0, 191.0, 440.0, 854.0)
actions: scrollToOffset, scrollUp
flags: hasImplicitScrolling
scrollExtentMin: 0.0
scrollPosition: 0.0
scrollExtentMax: 547.8
...
(children below this point — ordinary buttons and list rows further down the screen — are all flagged `isHidden` while off-screen, and never receive VoiceOver focus via single-finger swipe — only via a manual 3 finger scroll first)
Flutter Doctor output
`flutter doctor -v` (3.44.0; 3.47.4 also tested and reproduces)
[✓] Flutter (Channel stable, 3.44.0, on macOS 15.7.4 24G517 darwin-arm64, locale en-GB) [616ms]
• Flutter version 3.44.0 on channel stable at /Users/jamie/fvm/versions/3.44.0
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 559ffa3f75 (4 months ago), 2026-05-15 14:13:13 -0700
• Engine revision 4c525dac5e
• Dart version 3.12.0
• DevTools version 2.57.0
[✓] Xcode - develop for iOS and macOS (Xcode 26.3) [1,310ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 17C529
• CocoaPods version 1.16.2
[✓] Connected device (5 available) [6.3s]
• iPhone1 (mobile) • ios • iOS 27.0 24A437 <- reproduces here
• iPhone2 (mobile) • ios • iOS 26.6.2 23G90 <- does not reproduce here
• No issues found!
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone

Activity
mbcorona commentedon Sep 15, 2026
Hi @J-Mowbray, thanks, excellent repro and diagnosis.
Routing to the accessibility team for evaluation.
J-Mowbray commentedon Sep 15, 2026
Thanks, very much appreciated! 😊
zywek123 commentedon Sep 16, 2026
Confirmed in application built with flutter 3.38 and earlier.
uditswaroopa09 commentedon Sep 18, 2026
agree facing the same issue as above thanks for reporting @J-Mowbray
zywek123 commentedon Sep 21, 2026
Following up on this with data from two devices, because I think we can narrow the cause down
to a single line in the engine.
Thanks @mbcorona for routing it so quickly — hopefully this saves the accessibility team some
digging.
Same screen, two OS versions
I instrumented
AppDelegatewith an observer forUIAccessibilityElementFocusedNotificationthat logs the class, label and
accessibilityFrameof every element VoiceOver focuses. Thescreen is a
ListView.builderof nearby transit stops, roughly 40 items, each 76–94 pt tall.iOS 16 (works). The pattern repeats for every item:
Note there is never a log line with an empty frame: the scroll happens before focus moves.
UIKit takes the next candidate (a node the framework marks
isHidden, which the engine handsout with
CGRectZero), callsaccessibilityScrollToVisibleon it, the engine callsshowOnScreen, Dart scrolls, and only then does the node get a real frame and the focus.iOS 27.2 beta (broken). Swiping right walks 147 → 223 → 299 → 375 → 469, and then:
That 38 pt scroll is the interesting part.
Świderska 03sat at 545–639 while the list viewportends around 601 — it was clipped, not hidden, so it still had a real frame, and iOS 27 did
scroll it into view. The item after it was fully outside the viewport, so it was
isHiddenwithan empty frame, and VoiceOver walked straight out of the list into the switch below it.
What actually changed
iOS 27 did not drop
accessibilityScrollToVisible— the 38 pt nudge proves it still fires.What changed is the step before: a node whose
accessibilityFrameisCGRectZerois no longerconsidered a candidate for the next element, so the code path that would have scrolled it into
view is never reached. Elements that are merely clipped by their scroll view behave exactly as
they did on iOS 26 and earlier.
That makes this squarely a Flutter-side fragility rather than a VoiceOver bug: UIKit's own scroll
views never expose off-screen cells this way, so nothing in Apple's frameworks depended on empty
frames being focusable. Flutter does, right here in
SemanticsObject.mm:- (CGRect)accessibilityFrame { if (self.node.flags.isHidden) { return [super accessibilityFrame]; // nothing ever sets it → CGRectZero } return [self globalRect]; }Combined with
isFocusable, which deliberately keeps these nodes focusable:return (self.node.flags.hasImplicitScrolling && self.node.flags.isHidden) || ...we keep the node in the tree as a focus target, but hand VoiceOver a zero rect to aim at. On
iOS 26 that was enough; on iOS 27 it is not.
Suggested fix
Give hidden-but-focusable nodes a non-empty frame instead of
CGRectZero— e.g. the node'sglobalRectclamped to a thin strip on the leading or trailing edge of the nearest scrollableancestor's visible rect, depending on which side of the viewport the node lives on. They then
look like clipped elements, which is the case iOS 27 still handles, and
accessibilityScrollToVisible→
showOnScreentakes over unchanged. No new API adoption, and nothing changes for iOS 26 andearlier, where clipped elements were already handled the same way.
I have not found anything in Apple's documentation that covers this: the iOS 27 release notes
have no Accessibility section at all and no mention of
UIAccessibility, and the UIKit updatesfor June/September 2026 are silent on it too. The documented iOS 27 accessibility additions
(
accessibilityLinkedGroup,causesPageTurn+accessibilityScroll,UITextInputfor renderedtext) are all unrelated. So this looks like an undocumented change to VoiceOver's candidate
filtering rather than a deprecation with a migration path.
Workaround for anyone blocked right now
Until the engine change lands, this can be worked around in the app: swizzle
-[SemanticsObject accessibilityFrame]and, whenever the original returns an empty rect,substitute a few-point strip at the edge of the nearest
FlutterScrollableSemanticsObject'svisible rect. That restores implicit scrolling for every scrollable in the app —
ListViewandSingleChildScrollViewalike — without touching the semantics tree from Dart. Ugly, but it is asingle place, and it can be gated on iOS 27+ so older systems keep the stock behaviour.
For context on why this matters beyond convenience: our app is a navigation aid for blind users,
where VoiceOver is the interface. On iOS 27 everything below the first screenful of any list is
reachable only via the three-finger scroll gesture, which many users never discovered because
they never needed it.
9 remaining items
jmagman commentedon Sep 21, 2026
Is this related to #98572 ?
zywek123 commentedon Sep 22, 2026
@jmagman Not that issue. Issue #98572, which you mentioned, is related to #189285.
zywek123 commentedon Sep 22, 2026
@mbcorona @LongCatIsLooong Follow-up after a day of testing this on a real iOS 27 device, in case it saves whoever picks this
up some time. There is a second failure mode here that the empty-frame explanation does not
cover, and it survives every app-side workaround I could think of.
Extra repro requirement
Put a row of buttons directly below the list (in our case a toolbar: filter, more, refresh).
The bug needs a focusable element outside the scrollable, underneath it.
Only a three-finger scroll or several left swipes "unstick" it, after which it works again until
the next time focus leaves the list.
Why this isn't just the
CGRectZeroissueAt step 4 the next list item is present in the semantics tree and visible on screen. Logged
from a swizzled
-[SemanticsObject accessibilityElementDidBecomeFocused]:The list viewport ends at ~601, the toolbar starts at 611, and items 12–18 exist. So there is a
real, visible, focusable next element and VoiceOver walks past it.
The decisive observation: in this state VoiceOver asks the app nothing. I swizzled and logged
accessibilityFrame,indexOfAccessibilityElement:andaccessibilityElementCount— all threeare queried on screen load and after a refresh, but not once during step 4. Every app-side hook is
an answer to a question, and there are no questions.
What was tried (all ineffective, all reverted)
showOnScreenon the off-screen neighbour, on every focus eventshowOnScreenonly when focus re-enters the scrollableUIAccessibilityLayoutChangedNotification, argumentnilUIAccessibilityLayoutChangedNotification, argument = focused elementindexOfAccessibilityElement:(it only matches direct children, while focus sits on a grandchild — the tile has one labelled child)accessibilityContainerType=UIAccessibilityContainerTypeSemanticGroupon the scrollable's containerUIAccessibilityScreenChangedNotificationwith the focused element as argumentThat last row matters: if this were VoiceOver holding a stale screen model,
ScreenChangedwouldhave rebuilt it.
The one thing that did fix it
Mid-session an incoming system notification banner appeared, the screen flickered — and without
touching focus, from the same row, swiping right started scrolling the list again.
Since
ScreenChangeddoes not reproduce that effect, the banner most likely worked by forcing afull semantics rebuild on the Flutter side, not by refreshing anything in the system. If that
reading is right, the fix belongs in the framework/engine rather than in app code: something that
re-emits the semantics tree when focus comes back into a scrollable. Worth checking whether a
markNeedsSemanticsUpdate()on the viewport at that moment reproduces the banner's effect — thatis the one lead I could not test from outside the engine, since the app has no reliable signal for
"focus re-entered the list" other than the private focus callback I was swizzling.
Tested on Flutter 3.47.5 (engine 16 Sep 2026), iOS 27.0 and 27.2 beta, iPhone with VoiceOver.
Happy to run patches against this app — the screens involved use both
ListViewandSingleChildScrollView, with blind users on them daily.BLKKKBVSIK commentedon Sep 30, 2026
Is there an estimated timeline for bringing this fix to the master channel?
This is a critical concern for our application, which is specifically built for people with disabilities, particularly visually impaired users.
BLKKKBVSIK commentedon Oct 1, 2026
I made a flutter fork with a very temporary (and vibecoded) fix.
It should fix the issue for iOS, I haven't tried it on MacOS.
I will not make a PR out of this fix for the following reasons:
But if you are willing to make a PR, you can base yourself on my fix.
Fork is available here 👇
https://github.com/BLKKKBVSIK/flutter-sdk-ios27-fix-a11y
[iOS] Give hidden semantics nodes in scrollables a non-empty accessib…
luomo-pro commentedon Oct 27, 2026
Is there any progress on this issue now? I don't think it has been merged into the master yet
davefaliskie commentedon Oct 27, 2026
I believe this will go live with flutter 3.50 based on this #193723 (comment)
haashem commentedon Nov 2, 2026
We have the same issue when we turn on
Full Keyboard AccessfromSettings/Accessibility/Keyboard & Typing/Full Keyboard AccessSteps to reproduce:
1- Connect external bluetooth keyboard to your iOS device
2- Turn on Full Keyboard Access from path above
3- Try to navigate this sample app using keyboard arrow keys.
Actual Results:
Focused Tile doesn't scroll to the view!
Expected Result:
Tile Should scroll to the screen.
If tile is not partially visible it can't gain keyboard focus and scroll to the view.
Q:
Will #193723 or #195731 also cover horizontal scrollables and FKA arrow-key navigation? Or should I open a separate issue for the horizontal FKA case?
ScreenRecording_11-02-2026.14-00-31_1.mov
[iOS a11y] Use a real `UIScrollView` as a11y scrollable container (fl…
LongCatIsLooong commentedon Nov 8, 2026
I merged #195731 over #193723 since I'm concerned about giving
hiddensemantic objects non-empty accessibility frames. I suspect that would affect a11y hit-testing and Voice Control (those hidden items will get a label under Voice Control). @zywek123 could you try #195731 out (it should be in the master channel) and see if that fixes the toolbar case?zywek123 commentedon Nov 12, 2026
@LongCatIsLooong It works – you can go back to the list of items and swipe out to the bar – but that’s not how it’s supposed to work.
The list should continue scrolling even after returning to it from the bar, right up to the last item visible on the screen; that’s how it was in iOS 26. Flicking from the bar back to the list, then flicking right would bring up the next items in the list, rather than jumping straight back to the bar.
However, scrolling through the list using flicks works as it should; when I start from the first visible item, it scrolls correctly and I can go through the entire list.