Skip to content

[iOS 27] VoiceOver's implicit-scroll-into-view no longer works - single-finger swipe never advances past the visible boundary of a Scrollable #192774

Description

@J-Mowbray

Steps to reproduce

  1. Build and run any app containing a Scrollable (ListView,SingleChildScrollView, etc.) with more content than fits in one screen. See minimal repro below (confirmed).

  2. On a physical iPhone running iOS 27.0, turn on VoiceOver (Settings > Accessibility > VoiceOver).

  3. 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.

  4. 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 + Column layout, and separately a plain ListView both affected identically, so this isn't specific to one Scrollable variant).
  • 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

Brief screen recording illustrating the issue

Screen recording

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!

Activity

  1. added
    in triagePresently being triaged by the triage team
    platform-iosiOS applications specifically
    a: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    fyi-iosFor the attention of iOS platform team
    team-accessibilityOwned by Framework Accessibility team (i.e. responsible for accessibility code in flutter/flutter)
    and removed
    in triagePresently being triaged by the triage team
    on Sep 15, 2026
  2. mbcorona commented on Sep 15, 2026

    @mbcorona
    Member

    Hi @J-Mowbray, thanks, excellent repro and diagnosis.
    Routing to the accessibility team for evaluation.

  3. J-Mowbray commented on Sep 15, 2026

    @J-Mowbray
    Author

    Hi @J-Mowbray, thanks, excellent repro and diagnosis.
    Routing to the accessibility team for evaluation.

    Thanks, very much appreciated! 😊

  4. zywek123 commented on Sep 16, 2026

    @zywek123

    Confirmed in application built with flutter 3.38 and earlier.

  5. uditswaroopa09 commented on Sep 18, 2026

    @uditswaroopa09

    agree facing the same issue as above thanks for reporting @J-Mowbray

  6. zywek123 commented on Sep 21, 2026

    @zywek123

    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 AppDelegate with an observer for UIAccessibilityElementFocusedNotification
    that logs the class, label and accessibilityFrame of every element VoiceOver focuses. The
    screen is a ListView.builder of nearby transit stops, roughly 40 items, each 76–94 pt tall.

    iOS 16 (works). The pattern repeats for every item:

    [VO] 'Świderska 01, ...' frame=(0.0, 618.0, 375.0, 94.0)  ->  <nil>     // entering, bottom edge
    [VO] 'Świderska 01, ...' frame=(0.0, 524.0, 375.0, 94.0)  ->  <nil>     // leaving, list moved 94 pt
    [VO] <nil>  ->  'Świderska 04, ...' frame=(0.0, 618.0, 375.0, 94.0)     // next item, bottom edge
    

    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 hands
    out with CGRectZero), calls accessibilityScrollToVisible on it, the engine calls
    showOnScreen, 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:

    [VO] 'Przystanek tramwajowy, ...' frame=(0.0, 431.0, 375.0, 76.0)  ->  <nil>   // entered at 469, scrolled 38 pt
    [VO] <nil>  ->  'Świderska 03, ...' frame=(0.0, 507.0, 375.0, 94.0)
    [VO] 'Świderska 03, ...' frame=(0.0, 507.0, 375.0, 94.0)  ->  <nil>
    [VO] <nil>  ->  FlutterSwitchSemanticsObject 'Filtruj wg kierunku' frame=(8.0, 611.0, 89.8, 48.0)
    

    That 38 pt scroll is the interesting part. Świderska 03 sat at 545–639 while the list viewport
    ends 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 isHidden with
    an 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 accessibilityFrame is CGRectZero is no longer
    considered 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's
    globalRect clamped to a thin strip on the leading or trailing edge of the nearest scrollable
    ancestor'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
    → showOnScreen takes over unchanged. No new API adoption, and nothing changes for iOS 26 and
    earlier, 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 updates
    for June/September 2026 are silent on it too. The documented iOS 27 accessibility additions
    (accessibilityLinkedGroup, causesPageTurn + accessibilityScroll, UITextInput for rendered
    text) 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's
    visible rect. That restores implicit scrolling for every scrollable in the app — ListView and
    SingleChildScrollView alike — without touching the semantics tree from Dart. Ugly, but it is a
    single 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.

  7. added
    c: regressionIt was better in the past than it is now
    and removed
    fyi-iosFor the attention of iOS platform team
    team-accessibilityOwned by Framework Accessibility team (i.e. responsible for accessibility code in flutter/flutter)
    on Sep 21, 2026
  8. 9 remaining items

  9. jmagman commented on Sep 21, 2026

    @jmagman
    Member

    Is this related to #98572 ?

  10. zywek123 commented on Sep 22, 2026

    @zywek123

    @jmagman Not that issue. Issue #98572, which you mentioned, is related to #189285.

  11. zywek123 commented on Sep 22, 2026

    @zywek123

    @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.

    1. Swipe right through the list — with the frame workaround in place this scrolls fine.
    2. Keep going until focus leaves the list and lands on the toolbar below.
    3. Swipe left to come back into the list. Focus lands on the last visible row.
    4. Swipe right. Focus jumps straight back to the toolbar, skipping the rest of the list.

    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 CGRectZero issue

    At step 4 the next list item is present in the semantics tree and visible on screen. Logged
    from a swizzled -[SemanticsObject accessibilityElementDidBecomeFocused]:

    'Przystanek tramwajowy, 660 m'  frame=(0, 529, 375, 72)  item 11 of 19   ← focus here
    'Filtruj wg kierunku'           frame=(8, 611,  90, 48)  not in list     ← after swipe right
    

    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: and accessibilityElementCount — all three
    are 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)

    Attempt Result
    Edge strip for empty-frame nodes (the workaround from my previous comment) Fixes plain swiping; no effect on this scenario
    Same for nodes whose frame lies entirely outside the list viewport (an item just below the list still intersects the screen, so it needs the viewport, not the screen, as reference) Correct and worth having, symptom unchanged
    showOnScreen on the off-screen neighbour, on every focus event Feedback loop with touch exploration — a finger resting on the list edge scrolls it continuously
    showOnScreen only when focus re-enters the scrollable No effect; the neighbour is often already visible anyway
    UIAccessibilityLayoutChangedNotification, argument nil Ignored
    UIAccessibilityLayoutChangedNotification, argument = focused element Ignored
    Fixing indexOfAccessibilityElement: (it only matches direct children, while focus sits on a grandchild — the tile has one labelled child) Sound fix in principle, but the method is never called in this scenario
    accessibilityContainerType = UIAccessibilityContainerTypeSemanticGroup on the scrollable's container No effect
    UIAccessibilityScreenChangedNotification with the focused element as argument Screen-change sound plays, speech is interrupted, navigation unchanged

    That last row matters: if this were VoiceOver holding a stale screen model, ScreenChanged would
    have 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 ScreenChanged does not reproduce that effect, the banner most likely worked by forcing a
    full 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 — that
    is 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 ListView and
    SingleChildScrollView, with blind users on them daily.

  12. BLKKKBVSIK commented on Sep 30, 2026

    @BLKKKBVSIK

    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.

  13. BLKKKBVSIK commented on Oct 1, 2026

    @BLKKKBVSIK

    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:

    • I'm already late enough on what I should do
    • Pushing a PR on Flutter/Flutter imo is a nightmare.

    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

  14. luomo-pro commented on Oct 27, 2026

    @luomo-pro

    Is there any progress on this issue now? I don't think it has been merged into the master yet

  15. davefaliskie commented on Oct 27, 2026

    @davefaliskie

    20. Is there any progress on this issue now? I don't think it has been merged into the master yet

    I believe this will go live with flutter 3.50 based on this #193723 (comment)

  16. haashem commented on Nov 2, 2026

    @haashem

    We have the same issue when we turn on Full Keyboard Access from Settings/Accessibility/Keyboard & Typing/Full Keyboard Access

    Steps 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
    import 'package:flutter/material.dart';
    
    void main() => runApp(const HorizontalListApp());
    
    class HorizontalListApp extends StatelessWidget {
      const HorizontalListApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: const Text('Horizontal List')),
            body: SizedBox(
              height: 182,
              child: ListView.separated(
                scrollDirection: Axis.horizontal,
                padding: const EdgeInsets.all(16),
                itemCount: 20,
                separatorBuilder: (context, index) => const SizedBox(width: 12),
                itemBuilder: (context, index) => SizedBox(
                  width: 150,
                  child: ListTile(
                    shape: RoundedRectangleBorder(
                      side: const BorderSide(),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    title: Text('Item ${index + 1}'),
                    onTap: () {
                      ScaffoldMessenger.of(context)
                        ..hideCurrentSnackBar()
                        ..showSnackBar(
                          SnackBar(content: Text('Item ${index + 1} selected')),
                        );
                    },
                  ),
                ),
              ),
            ),
          ),
        );
      }
    }
    
  17. LongCatIsLooong commented on Nov 8, 2026

    @LongCatIsLooong
    Contributor

    I merged #195731 over #193723 since I'm concerned about giving hidden semantic 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?

  18. zywek123 commented on Nov 12, 2026

    @zywek123

    @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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

P1High-priority issues at the top of the work lista: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)c: regressionIt was better in the past than it is nowcustomer: bluefyi-accessibilityFor the attention of Framework Accessibility teamhas reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-iosiOS applications specificallyteam-iosOwned by iOS platform teamtriaged-iosTriaged by iOS platform team

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions