Repository navigation
Description
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 -idoes 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
pressrefusal says the target is off screen and suggestsscroll <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
thymikee commentedon Nov 12, 2026
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.fix(ios): mark zero-area nodes unmeasured and point their press refus…