Repository navigation
Description
flutter-semantics-hit-test-repro.zip
Draft report; not submitted.
Steps to reproduce
- Use the attached Flutter-only project, or create a web project and replace
lib/main.dartwith the code below. - Keep/add
<meta name="viewport" content="width=device-width, initial-scale=1.0">in the HTML head before Flutter bootstrap, as in the attachedweb/index.html. - Run
flutter pub get, thenflutter build web --release. - Serve
build/web, for example withpython3 -m http.server 4387 --bind 127.0.0.1 --directory build/web. - Open
http://127.0.0.1:4387/in Safari on an iPhone 14 simulator, portrait, at 100% zoom. The tested layout viewport is 390 × 699 CSS pixels. - Tap the centre of the visible blue LOWER button once. No scrolling or text-field focus is needed.
Tested with Flutter 3.47.1 stable, framework 6655482ec0, engine 5d53178869, Dart 3.13.1, CanvasKit, Safari 26.4 on iOS simulator runtime 26.4.1 (23E254a), Xcode 26.4.1. Semantics are explicitly enabled with ensureSemantics(); VoiceOver is not needed. The semantics handle is retained for the app lifetime.
Expected results
Only LOWER's callback runs: UPPER callbacks: 0, LOWER callbacks: 1, Last callback: LOWER.
Actual results
UPPER's callback runs instead: UPPER callbacks: 1, LOWER callbacks: 0, Last callback: UPPER.
Repeated native taps on the lower control continue incrementing UPPER. It also reproduces after reloading, without any Inspector diagnostics installed. The app uses only Flutter widgets and has no custom scaling, fonts, CSS, backend, authentication, or third-party runtime packages.
Controls and browser measurements
Using the same build and native lower-button tap:
| Configuration | Result |
|---|---|
| Default: long label, semantics enabled, separate SliverToBoxAdapters | UPPER callback (incorrect) |
?label=short: only change label to “Upper” |
LOWER callback |
?semantics=off: omit explicit semantics activation |
LOWER callback; zero flt-semantics DOM elements verified |
?layout=column: long label, semantics enabled, Column |
LOWER callback |
The short-label case retains 13 semantic DOM elements. These are diagnostic comparisons, not suggested accessibility workarounds.
In the failing case, an optional passive pointerdown listener recorded client coordinates (340,307) with target flt-semantic-node-9 (UPPER), outside that element's box and inside LOWER's box:
| Measurement | UPPER | LOWER |
|---|---|---|
| Element rectangle (x, y, width, height) | 314, 129, 52, 52 | 314, 281, 52, 52 |
| Text Range rectangle | 314, 129, 75.515625, 459 | 314, 281, 41.7578125, 19 |
| z-index | 5 | 3 |
| overflow | visible | visible |
document.elementsFromPoint(340,307) returns UPPER before LOWER. Viewport scale is 1, with zero viewport offsets and window scroll. The upper accessible label is invisible but its wrapped text overflows far below the upper element and participates in Safari's hit test. Flutter then invokes that node's tap callback.
In the Column comparison, UPPER/LOWER z-indices are 4/6 and the correct lower callback runs. These observations point to Flutter web semantics text overflow and stacking, rather than a visual/pointer scale mismatch. They do not establish which engine or browser change is the best remedy.
Code sample
The complete lib/main.dart follows. The HTML viewport meta tag above is also included in the attached project.
main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
const String longLabel =
'Upper control with a deliberately long accessible description. '
'This entire description belongs only to the upper control. '
'It must never intercept a tap on the separate lower control.';
SemanticsHandle? semanticsHandle;
void main() {
WidgetsFlutterBinding.ensureInitialized();
final Map<String, String> parameters = Uri.base.queryParameters;
final bool enableSemantics = parameters['semantics'] != 'off';
final bool useLongLabel = parameters['label'] != 'short';
if (kIsWeb && enableSemantics) {
semanticsHandle = SemanticsBinding.instance.ensureSemantics();
}
runApp(
ReproducerApp(
enableSemantics: enableSemantics,
useLongLabel: useLongLabel,
layout: parameters['layout'] ?? 'slivers',
),
);
}
class ReproducerApp extends StatelessWidget {
const ReproducerApp({
super.key,
required this.enableSemantics,
required this.useLongLabel,
required this.layout,
});
final bool enableSemantics;
final bool useLongLabel;
final String layout;
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: ReproducerScreen(
enableSemantics: enableSemantics,
useLongLabel: useLongLabel,
layout: layout,
),
);
}
}
class ReproducerScreen extends StatefulWidget {
const ReproducerScreen({
super.key,
required this.enableSemantics,
required this.useLongLabel,
required this.layout,
});
final bool enableSemantics;
final bool useLongLabel;
final String layout;
@override
State<ReproducerScreen> createState() => _ReproducerScreenState();
}
class _ReproducerScreenState extends State<ReproducerScreen> {
int upperCount = 0;
int lowerCount = 0;
String lastCallback = 'none';
void recordTap(String control) {
setState(() {
lastCallback = control;
if (control == 'UPPER') {
upperCount++;
} else {
lowerCount++;
}
});
debugPrint('CALLBACK $control upper=$upperCount lower=$lowerCount');
}
Widget buildControl(String name, String accessibleLabel, Color color) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(name),
Semantics(
container: true,
button: true,
label: accessibleLabel,
child: Material(
color: color,
child: InkWell(
key: ValueKey<String>(name),
onTap: () => recordTap(name),
child: SizedBox(
width: 52,
height: 52,
child: ExcludeSemantics(
child: Center(
child: Text(
name.substring(0, 1),
style: const TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: buildLayout(<Widget>[
const Text(
'Tap the blue LOWER button',
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 12),
Text(
'Semantics: ${widget.enableSemantics ? 'on' : 'off'}; '
'label: ${widget.useLongLabel ? 'long' : 'short'}\n'
'Layout: ${widget.layout}',
),
const SizedBox(height: 24),
buildControl(
'UPPER',
widget.useLongLabel ? longLabel : 'Upper',
Colors.teal,
),
const SizedBox(height: 100),
buildControl('LOWER', 'Lower', Colors.blue),
const SizedBox(height: 32),
Text(
'UPPER callbacks: $upperCount\n'
'LOWER callbacks: $lowerCount\n'
'Last callback: $lastCallback',
style: const TextStyle(fontSize: 20),
),
]),
),
),
);
}
Widget buildLayout(List<Widget> children) {
return switch (widget.layout) {
'column' => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
'list' => ListView(children: children),
_ => CustomScrollView(
slivers: children
.map((Widget child) => SliverToBoxAdapter(child: child))
.toList(),
),
};
}
}
Screenshots
The source bundle includes these images. Upload them with the issue when submitting; the relative links below are for local review.
Wrong callback after one native tap on LOWER:

Logs
CALLBACK UPPER upper=1 lower=0
CALLBACK UPPER upper=2 lower=0
Native pointerdown:
client: (340, 307)
target: flt-semantic-node-9 (UPPER)
target element: (314, 129, 52, 52)
LOWER element: (314, 281, 52, 52)
Hit targets at (340, 307):
flt-semantic-node-9, flt-semantic-node-7, FLUTTER-VIEW, BODY, HTML
See evidence/safari-long-label-console.txt for the captured Inspector output and inspect.js for optional passive diagnostics. Some long Inspector entries are truncated by its accessibility interface; the compact geometry and target entries are complete. No DOM styles or Flutter engine code were modified during the failing test.
flutter analyze passed. The release web build succeeded.
Scope and related reports
Confirmed with native interactions in an iPhone simulator's Safari, not a physical device. Other browsers, Android, VoiceOver, and Flutter master have not been tested. No exact duplicate was established in the issue search.
Related semantics/tap-routing reports include #129324, #154741, and the historical #134842. Their symptoms differ; this report isolates two buttons and overflowing accessible text without text input or navigation.
Flutter Doctor output
Local user paths and simulator IDs are redacted.
flutter doctor -v
[✓] Flutter (Channel stable, 3.47.1, on macOS 26.6.2 25G83 darwin-arm64, locale de-DE) [412ms]
• Flutter version 3.47.1 on channel stable at <FLUTTER_SDK>
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 6655482ec0 (3 weeks ago), 2026-08-19 10:07:23 -0700
• Engine revision 5d53178869
• Dart version 3.13.1
• DevTools version 2.60.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-record-use, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [1.647ms]
• Android SDK at /opt/homebrew/share/android-commandlinetools
• Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
• Platform android-36, build-tools 36.0.0
• ANDROID_HOME = /opt/homebrew/share/android-commandlinetools
• ANDROID_SDK_ROOT = /opt/homebrew/share/android-commandlinetools
• Java binary at: /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/bin/java
This JDK is specified in your Flutter configuration.
To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment Homebrew (build 17.0.19+0)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 26.4.1) [942ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 17E202
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [5ms]
• CHROME_EXECUTABLE = <USER_HOME>/Applications/Chromium.app/Contents/MacOS/Chromium
[✓] Connected device (5 available) [6,0s]
• iPhone 17 Pro (mobile) • <DEVICE_ID> • ios • com.apple.CoreSimulator.SimRuntime.iOS-26-4 (simulator)
• Codex iPhone 14 Safari (mobile) • <DEVICE_ID> • ios • com.apple.CoreSimulator.SimRuntime.iOS-26-4 (simulator)
• iPad Pro 13-inch (M5) (mobile) • <DEVICE_ID> • ios • com.apple.CoreSimulator.SimRuntime.iOS-26-4 (simulator)
• macOS (desktop) • macos • darwin-arm64 • macOS 26.6.2 25G83 darwin-arm64
• Chrome (web) • chrome • web-javascript • Chromium 149.0.7805.0
[✓] Network resources [580ms]
• All expected network resources are available.
• No issues found!




Activity
saurabh-mirajkar commentedon Sep 9, 2026
@konsultaner Thanks for the report. I was able to reproduce this on latest stable and iOS Safari using the provided reproduction. Labelling this for team to investigate further.
konsultaner commentedon Oct 8, 2026
@flutter-zl Can i do something to speed up the development of a fix? should i try to work on a PR?
flutter-zl commentedon Oct 8, 2026
PR #194011 has been created and will be reviewed soon. Thanks for bringing this up!
[web] Clip semantics DOM text labels to stop stealing taps (flutter#1…
konsultaner commentedon Oct 26, 2026
Is this in the current Release or will it be in the next one?
flutter-zl commentedon Oct 26, 2026
The fix, #194011, landed after the 3.47 stable release, so it is not in 3.47.x. It is included in the 3.50 beta, starting with 3.50.0-0.1.pre, and will be part of the next stable release, 3.50.
If you want to verify it before then, you can switch to the beta channel with
flutter channel betaandflutter upgrade, rebuild your web app, and try the repro again. Please let me know if you still see the issue there.[web] Clip semantics DOM text labels to stop stealing taps (flutter#1…
github-actions commentedon Nov 9, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.