Skip to content

MacOS - bug system sound on tap inside RouterOutlet #984

Description

@alimardonbegov

Describe the bug

Title

System sound on tap inside RouterOutlet

Platform

Applicable only to macOS (my version 15.3.1).

Description

I encountered this issue while using modular with RouterOutlet in a new Flutter project. The RouterOutlet is placed inside the widget tree and occupies the remaining screen space using Expanded(child: RouterOutlet()), where nested navigation takes place. It’s also done this way in the official documentation.

However, depending on the widget inside the RouterOutlet, some parts of the RouterOutlet area become unexpectedly clickable with a sound effect. This also applies when there's another nested RouterOutlet inside it.

The behavior is inconsistent:

  • Text('123') inside RouterOutlet is not clickable with a sound.
  • Row(children: [Text('123')]) inside RouterOutlet is clickable with a sound, except for the text itself.

Using Flutter DevTools, I noticed that the clicks triggering the sound effect are intercepted by a Listener widget inside RouterOutlet. However, this was determined indirectly, as the trigger doesn’t always occur precisely on that widget.

I checked for similar issues in both modular and Flutter repositories but couldn’t find anything relevant.

Question

Is there a way to disable this sound effect? I couldn’t find any relevant settings or documentation on this.

Environment
Add your flutter doctor -v
Image

Add your pubspec.yaml
Image

Add your Info.plist
Image

Add your AppDelegate.swift
Image

Add your MainActivity
not applicable

Add your AndroidManifest.xml
not applicable

Add your /app/res/values/strings.xml
not applicable

To Reproduce


import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';

void main() {
 runApp(ModularApp(module: AppModule(), child: AppWidget()));
}


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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      theme: ThemeData(primarySwatch: Colors.blue),
      routerConfig: Modular.routerConfig,
    ); 
  }
}

class AppModule extends Module {
  AppModule();

  @override
  void routes(r){
    r.child(
      '/', child: (_) => HomePage(),children: [
        ChildRoute('/text', child: (context) => Text('123')),
        ChildRoute('/row', child: (context) => Row(children: [Text('123')],)),
        ModuleRoute(SecondModule.route, module: SecondModule()),
      ],
      transition: TransitionType.noTransition,
    );
  
    super.routes(r);
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SizedBox(
            width: 200,
            child: Column(
              children: [
                InkWell(
                  child: Text('Text'),
                  onTap: () => Modular.to.navigate('/text'),
                ),
                InkWell(
                  child: Text('Row'),
                  onTap: () => Modular.to.navigate('/row'),
                ),
                 InkWell(
                  child: Text('Submodule'),
                  onTap: () => Modular.to.navigate(SecondModule.route),
                ),
              ],
            ),
          ),
          Container(width: 1, color: Colors.black),
          Expanded(child: RouterOutlet()),
        ],
      ),
    );
  }
}

class SecondModule extends Module {
  static const route = '/second';
  @override
  void binds(i) {}

  @override
  void routes(r) {
    r.child('/', child: (context) => SecondPage(), children: [
      ChildRoute('/text', child: (context) => Text('123')),
      ChildRoute('/row', child: (context) => Row(children: [Text('123')],)),
    ]);
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Row(children: [
      SizedBox(
        width: 200, 
        child: Material(
          child: Column(
            children: [
              InkWell(
                child: Text('Text'),
                onTap: () => Modular.to.navigate('${SecondModule.route}/text'),
              ),
              InkWell(
                child: Text('Row'),
                onTap: () => Modular.to.navigate('${SecondModule.route}/row'),
              ),
            ],
          ),
        )
      ),
      Container(width: 1, color: Colors.black),
      Expanded(child: RouterOutlet()),
    ]);
  }
}

Expected behavior
Do not play unexpected system sound on mac os app

Screenshots
(turn on the sound on the video)

Screen.Recording.2025-02-18.at.12.03.44.mov

Activity

  1. tspoke commented on May 28, 2025

    @tspoke

    I have the exact same behaviour on Windows.

    I found a way to prevent it for now :

    return Stack(
          alignment: Alignment.topCenter,
          children: [
            Positioned.fill(child: GestureDetector(onTap: () {})), // click sound bug fix
            Column(...),
         ],
    );
  2. jacobaraujo7 commented on Jun 13, 2025

    @jacobaraujo7
    Contributor

    Fixed

  3. alimardonbegov commented on Jun 13, 2025

    @alimardonbegov
    Author

    Fixed

    Hello! Thanks for replying!

    Unfortunately, I still have the same issue.

    Flutter doctor:
    "
    Running flutter doctor...
    Doctor summary (to see all details, run flutter doctor -v):
    [✓] Flutter (Channel stable, 3.32.4, on macOS 15.5 24F74 darwin-arm64, locale en-ME)
    [✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
    [✓] Xcode - develop for iOS and macOS (Xcode 16.4)
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 2024.1)
    [✓] Connected device (2 available)
    [✓] Network resources

    • No issues found!
    "

    @jacobaraujo7 Is there a parameter I should use to avoid this, or is it supposed to disappear on its own?

    Screen.Recording.2025-06-13.at.17.55.05.mov
  4. tspoke commented on Mar 19, 2026

    @tspoke

    I confirm the bug is still present on Modular v6.4.1.

    Flutter 3.38.9 • channel stable • https://github.com/flutter/flutter.git
    Framework • revision 67323de285 (7 weeks ago) • 2026-01-28 13:43:12 -0800
    Engine • hash 5eb06b7ad5bb8cbc22c5230264c7a00ceac7674b (revision 587c18f873) (1 months ago) • 2026-01-27 23:23:03.000Z
    Tools • Dart 3.10.8 • DevTools 2.51.1

  5. tspoke commented on Oct 9, 2026

    @tspoke

    Still present on v7.

    Found the cause, and a workaround that covers every mouse button. The GestureDetector(onTap: () {}) workaround posted above only catches left clicks: right and middle clicks still beep.

    Cause

    RouterOutlet is a nested Navigator, and every route in a Navigator is built on top of a ModalBarrier. For page routes that barrier is non-dismissible, and when a non-dismissible barrier is tapped Flutter calls SystemSound.play(SystemSoundType.alert) (see handleDismiss in widgets/modal_barrier.dart). That is the alert beep on macOS and MessageBeep on Windows.

    A page that is not hit-test opaque lets the pointer through its empty areas, so the barrier underneath gets the tap. This explains the inconsistency in the report: a lone Text fills the outlet and always claims the hit, while Row(children: [Text]) only claims it on the text itself. It also happens on root routes whose page leaves empty areas.

    Workaround

    Make the page hit-test opaque so the tap never reaches the barrier.

    Per page (v6 syntax shown):

    ChildRoute(
      '/row',
      child: (context) => Listener(
        behavior: HitTestBehavior.opaque,
        child: Row(children: [Text('123')]),
      ),
    ),

    Once for the whole app (v7), with a PageTransition:

    class HitOpaquePageTransition extends PageTransition {
      const HitOpaquePageTransition();
    
      @override
      Page<void> buildPage(LocalKey key, Widget child) =>
          TransitionType.none.buildPage(key, Listener(behavior: HitTestBehavior.opaque, child: child));
    }
    
    ModularApp(
      module: appModule,
      defaultTransition: const HitOpaquePageTransition(),
      child: const AppWidget(),
    );

    Swap TransitionType.none for .material or .fade as needed. Routes that declare their own transition: need the same wrap.

    Tested on flutter_modular 7.1.1, Flutter 3.47.2, Windows. With TransitionType.none, a tap on an empty area sends SystemSound.play with SystemSoundType.alert on the platform channel, for both a root page and an outlet page. With the wrap, left, right and middle clicks send nothing, and the page's own widgets still receive their taps. The per-page wrap relies on the same mechanism on v6, but I only ran it on v7.

    Possible fix in modular

    Modular creates these pages itself (_TransitionPage and RouterOutletState._page in v7). Wrapping child there in an opaque hit-test widget, or returning a route whose buildModalBarrier() does not beep, would fix it for everyone.

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

    newNew issue request attention

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions