Skip to content

[web][iOS Safari] Long Semantics label overflows its button and intercepts taps on a later sliver #192466

Closed

Description

@konsultaner

flutter-semantics-hit-test-repro.zip

Draft report; not submitted.

Steps to reproduce

  1. Use the attached Flutter-only project, or create a web project and replace lib/main.dart with the code below.
  2. Keep/add <meta name="viewport" content="width=device-width, initial-scale=1.0"> in the HTML head before Flutter bootstrap, as in the attached web/index.html.
  3. Run flutter pub get, then flutter build web --release.
  4. Serve build/web, for example with python3 -m http.server 4387 --bind 127.0.0.1 --directory build/web.
  5. 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.
  6. 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.

Before Tap:
Image

Wrong callback after one native tap on LOWER:
Image

Short-label control:
Image

Semantics-off control:
Image

Column control:
Image

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

  1. saurabh-mirajkar commented on Sep 9, 2026

    @saurabh-mirajkar
    Contributor

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

  2. added
    a: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)
    platform-webWeb applications specifically
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    team-accessibilityOwned by Framework Accessibility team (i.e. responsible for accessibility code in flutter/flutter)
    fyi-webFor the attention of Web platform team
    and removed
    in triagePresently being triaged by the triage team
    on Sep 9, 2026
  3. self-assigned this
    on Sep 9, 2026
  4. added
    P1High-priority issues at the top of the work list
    triaged-webTriaged by Web platform team
    on Sep 9, 2026
  5. konsultaner commented on Oct 8, 2026

    @konsultaner
    Author

    @flutter-zl Can i do something to speed up the development of a fix? should i try to work on a PR?

  6. flutter-zl commented on Oct 8, 2026

    @flutter-zl
    Contributor

    @flutter-zl Can i do something to speed up the development of a fix? should i try to work on a PR?

    PR #194011 has been created and will be reviewed soon. Thanks for bringing this up!

  7. added a commit that references this issue on Oct 12, 2026
    aacaa41
  8. konsultaner commented on Oct 26, 2026

    @konsultaner
    Author

    Is this in the current Release or will it be in the next one?

  9. flutter-zl commented on Oct 26, 2026

    @flutter-zl
    Contributor

    13. Is this in the current Release or will it be in the next one?

    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 beta and flutter upgrade, rebuild your web app, and try the repro again. Please let me know if you still see the issue there.

  10. added a commit that references this issue on Oct 29, 2026
    2e2f36d
  11. github-actions commented on Nov 9, 2026

    @github-actions

    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 -v and a minimal reproduction of the issue.

  12. locked as resolved and limited conversation to collaborators on Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1High-priority issues at the top of the work lista: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)browser: safari-iosonly manifests in Safari on iOShas reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-webWeb applications specificallyteam-accessibilityOwned by Framework Accessibility team (i.e. responsible for accessibility code in flutter/flutter)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions