Skip to content

iOS: off-screen nodes with a zero-size rect are listed as on-screen, and pressing one does not point at scroll --until (Flutter repro) #4451

Closed

Description

@ruizemanuel

On iOS, a Flutter scroll view exposes its off-screen children to accessibility with a zero-size frame. snapshot -i lists them like any other node, so an agent cannot tell what is on screen, and press on one is refused with a message that does not say the target is off screen.

This is the next step after #3841, which made press refuse a zero-area target instead of tapping its origin (found there on SpringBoard, where off-screen page icons keep 0×0 rects). The refusal is right; what is missing is telling the agent why, and not presenting those nodes as visible in the first place. scroll down --until on the same target works on main since #4407.

Repro: a one-file Flutter app: a SingleChildScrollView with a tall block that pushes a "View all" button just below the fold.

Repro app: flutter create, then replace lib/main.dart with this (45 lines)
import 'package:flutter/material.dart';

void main() => runApp(const ReproApp());

class ReproApp extends StatelessWidget {
  const ReproApp({super.key});

  @override
  Widget build(BuildContext context) => const MaterialApp(home: ReproPage());
}

class ReproPage extends StatelessWidget {
  const ReproPage({super.key});

  @override
  Widget build(BuildContext context) {
    final screenHeight = MediaQuery.of(context).size.height;
    return Scaffold(
      appBar: AppBar(title: const Text('Repro')),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 1. An empty field whose only text is its hint.
            const Padding(
              padding: EdgeInsets.all(16),
              child: TextField(decoration: InputDecoration(hintText: 'Search')),
            ),
            // 2. Push the next widgets just below the fold.
            Container(
              height: screenHeight - 150,
              color: Colors.amber.shade100,
              alignment: Alignment.center,
              child: const Text('Tall block'),
            ),
            TextButton(onPressed: () {}, child: const Text('View all')),
            const Padding(padding: EdgeInsets.all(16), child: Text('Below the fold')),
            const SizedBox(height: 600),
            const Padding(padding: EdgeInsets.all(16), child: Text('Far below')),
          ],
        ),
      ),
    );
  }
}
$ agent-device open dev.repro.flutterA11yRepro --platform ios --relaunch
$ agent-device snapshot -i
Snapshot: 8 nodes
@e1 [application] "Flutter A11y Repro"
@e2 [other] "Repro"
@e3 [scroll-area] [scrollable]
@e4 [text-field] "Search" [editable]
@e5 [text] "Tall block"
@e6 [button] "View all"          <- below the fold
@e7 [text] "Below the fold"      <- below the fold
@e8 [text] "Far below"           <- 600 pt further down

$ agent-device press 'label="View all"'
Error (COMMAND_FAILED): Selector label="View all" resolved to invalid bounds. Not dispatched. Next: Read the diagnostics log for the failing step.

$ agent-device scroll down --until 'label="View all"'
Scrolled down 1 pass until label="View all" was visible

snapshot --json --raw for the three off-screen nodes: rect: {x: 0, y: 0, width: 0, height: 0}, hittable: false, role FlutterSemanticsObject. The on-screen nodes have real frames.

The same app on Android is presented correctly:

$ agent-device snapshot -i
Snapshot: 5 visible nodes
@e2 [group] "Repro"
@e3 [scroll-area] [scrollable]
  [content below scroll-area hidden]
@e4 [text-field] [editable]
@e5 [group] "Tall block"

Expected:

  • snapshot -i does not present these nodes as on-screen: drop them behind a [content … hidden] line as Android does, or mark them as the help legend describes ([off-screen below]).
  • The press refusal says the target is off screen and suggests scroll <direction> --until <selector>. The direction may be unknown; "off screen, scroll to reveal it" is still more useful than "invalid bounds".

Likely cause (a guess from reading the code): the off-screen classification in capture-kit/src/mobile-snapshot-semantics.ts works from geometry, and a zero-size rect at the origin carries none, so the node is neither clipped nor classified; resolution.ts then reports generic invalid bounds. A zero-size, non-hittable node under a [scrollable] ancestor could be treated as off screen with unknown direction.

Seen on: agent-device main at 2deb909 (0.21.37-dev) with Flutter 3.50.0 and 3.38.7, and 0.21.36 with Flutter 3.38.7; identical results · macOS 26.2 · Node 22.23.2 · Xcode 26.2 · iOS 26.2 simulator, iPhone 17 Pro · not tested on Xcode 27 / iOS 27

Activity

  1. thymikee commented on Nov 12, 2026

    @thymikee
    Member

    Triage: bug; actionable without more input from you. The zero-area press refusal from #3841 must stay: a 0×0 frame is not a safe touch point. The gap is snapshot presentation and recovery guidance: unmeasured nodes must not look like confirmed on-screen targets, and a target under a known scroll container should point at scroll --until. A zero frame alone does not establish a scroll direction.

  2. added a commit that references this issue on Nov 12, 2026
    0495ee1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions