Skip to main content

Command Palette

Search for a command to run...

The Ultimate Guide to Native macOS Menus in Flutter: Why mac_menu_bar Changes Everything

Updated
9 min read

Building a Flutter app for macOS? Your menu bar experience is about to get a serious upgrade.

Hey Flutter developers!

Let me ask you something: Have you ever spent hours fighting with Flutter's PlatformMenuBar, trying to make your macOS app's menus feel just right? Have you found yourself recreating the entire menu structure just to add one tiny item? Or worse, have you given up on custom clipboard handling because intercepting standard menu actions seemed impossible?

If you nodded along to any of these, you're not alone. I've been there. We've all been there.

But what if I told you there's a better way? A way that makes working with macOS menus feel natural, powerful, and dare I say... fun?

Let me introduce you to mac_menu_bar – a Flutter plugin that completely transforms how you work with macOS menus. By the end of this article, you'll understand why this isn't just another plugin, but a game-changer for macOS Flutter development.

Grab your favorite beverage, and let's dive in.

The PlatformMenuBar Problem (And Why It's Worse Than You Think)

Let's be honest about Flutter's built-in PlatformMenuBar. It's not bad – it's actually quite innovative. The declarative API fits Flutter's philosophy beautifully. You define your menus in code, and Flutter handles the platform-specific rendering.

Sounds perfect, right?

Wrong.

Here's what happens when you actually try to build a professional macOS app with PlatformMenuBar:

Problem #1: The "Rebuild Everything" Trap

Picture this: You're building a document editor. You want to add a single "Export to PDF" option to the File menu. Simple enough, right?

With PlatformMenuBar, you write this:

PlatformMenuBar(
  menus: [
    PlatformMenu(
      label: 'File',
      menus: [
        // Wait, I need ALL the standard items first...
        PlatformMenuItem(
          label: 'New',
          shortcut: SingleActivator(LogicalKeyboardKey.keyN, meta: true),
          onSelected: _onNew,
        ),
        PlatformMenuItem(
          label: 'Open...',
          shortcut: SingleActivator(LogicalKeyboardKey.keyO, meta: true),
          onSelected: _onOpen,
        ),
        PlatformMenuItem(
          label: 'Open Recent',
          // Oh god, this needs a submenu...
          menus: [
            // Do I really have to manage recent files myself?
          ],
        ),
        PlatformMenuDivider(),
        PlatformMenuItem(
          label: 'Close',
          shortcut: SingleActivator(LogicalKeyboardKey.keyW, meta: true),
          onSelected: _onClose,
        ),
        PlatformMenuItem(
          label: 'Save',
          shortcut: SingleActivator(LogicalKeyboardKey.keyS, meta: true),
          onSelected: _onSave,
        ),
        PlatformMenuItem(
          label: 'Save As...',
          shortcut: SingleActivator(
            LogicalKeyboardKey.keyS,
            meta: true,
            shift: true,
          ),
          onSelected: _onSaveAs,
        ),
        PlatformMenuItem(
          label: 'Revert to Saved',
          onSelected: _onRevert,
        ),
        PlatformMenuDivider(),
        // FINALLY! My custom item!
        PlatformMenuItem(
          label: 'Export to PDF...',
          shortcut: SingleActivator(
            LogicalKeyboardKey.keyE,
            meta: true,
            shift: true,
          ),
          onSelected: _onExportPDF,
        ),
        PlatformMenuDivider(),
        PlatformMenuItem(
          label: 'Page Setup...',
          onSelected: _onPageSetup,
        ),
        PlatformMenuItem(
          label: 'Print...',
          shortcut: SingleActivator(LogicalKeyboardKey.keyP, meta: true),
          onSelected: _onPrint,
        ),
      ],
    ),
    // And this is just the FILE menu!
    // I haven't even started on Edit, View, Window, Help...
  ],
)

That's 60+ lines of code just to add ONE menu item.

And here's the kicker: Miss one standard item, and your app feels broken to Mac users. Forget the keyboard shortcut for Print? Users notice. No "Recent Files" submenu? Feels amateur.

You're not building features anymore. You're maintaining boilerplate.

Problem #2: Standard Actions Are Locked Away

Let me share a real scenario that drove me crazy:

I was building a code editor. I wanted Copy to include syntax highlighting information in the clipboard – you know, so when you paste into Slack or a word processor, the code formatting is preserved.

Seems reasonable, right? It's 2024. Every professional Mac app does this.

But with PlatformMenuBar? There's literally no way to intercept the standard Edit menu Copy action. You can create your own Copy menu item, but the standard system one? Untouchable.

The "Hidden" Problem: User Expectations

Here's the thing that keeps me up at night: Mac users have expectations.

When they open your app, they expect:

  • A fully-featured File menu with New, Open, Save, Recent Files

  • An Edit menu with Undo, Redo, Cut, Copy, Paste, Select All

  • Standard keyboard shortcuts that work everywhere

  • Context-appropriate enabled/disabled states

  • Smooth, native performance

With PlatformMenuBar, meeting these expectations means writing hundreds of lines of boilerplate code. And the moment you need to customize something? You're in for a world of pain.

There has to be a better way.

Enter mac_menu_bar: A Paradigm Shift

Okay, enough complaining. Let's talk solutions.

The mac_menu_bar plugin takes a fundamentally different approach. Instead of recreating the macOS menu system in Flutter, it embraces it.

Think about it: macOS already has a perfect menu system. It's mature, performant, accessible, and users understand it. Why fight against it?

The Philosophy: Work With the Platform, Not Against It

Here's the core insight that makes mac_menu_bar special:

Your Flutter app runs inside a macOS application that already has a menu bar. Instead of replacing it, enhance it.

This single philosophical shift changes everything.

Let me show you that same example from earlier:

With mac_menu_bar:

await MacMenuBar.addMenuItem(
  menuId: 'File',
  itemId: 'export_pdf',
  title: 'Export to PDF...',
  shortcut: const SingleActivator(
    LogicalKeyboardKey.keyE,
    meta: true,
    shift: true,
  ),
);

MacMenuBar.setMenuItemSelectedHandler((itemId) {
  if (itemId == 'export_pdf') _onExportPDF();
});

That's it. 14 lines.

The File menu? Already there with all the standard items. The positioning? macOS handles it. The accessibility? Built-in. The native feel? Perfect.

You just added your custom item to the existing menu structure. Like a native macOS app would.

When I built mac_menu_bar, I started with a simple question: "Why are we rebuilding what macOS already does perfectly?"

That question led to a fundamental insight:

macOS menus work beautifully. We shouldn't replace them – we should enhance them.

It's like the difference between:

  • Building a house from scratch vs. renovating a room

  • Replacing your car's engine vs. adding a turbocharger

  • Rewriting Linux vs. writing a kernel module

Why rebuild the entire system when you can extend it intelligently? This philosophy guided every design decision in mac_menu_bar.

Feature Deep-Dive: What Makes This Special

Let's break down the features that make mac_menu_bar incredible. And I mean really break them down, with examples that show you the power.

Feature 1: Seamless Menu Integration

The Promise: Add items to existing macOS menus without recreating them.

Why It Matters: This is the headline feature, and it's revolutionary.

Real Example:

// Add to the File menu
await MacMenuBar.addMenuItem(
  menuId: 'File',
  itemId: 'quick_export',
  title: 'Quick Export',
  shortcut: const SingleActivator(
    LogicalKeyboardKey.keyE,
    meta: true,
  ),
);

// Add to the Edit menu
await MacMenuBar.addMenuItem(
  menuId: 'Edit',
  itemId: 'transform',
  title: 'Transform Selection...',
  shortcut: const SingleActivator(
    LogicalKeyboardKey.keyT,
    meta: true,
    shift: true,
  ),
);

// Add to the View menu
await MacMenuBar.addMenuItem(
  menuId: 'View',
  itemId: 'toggle_sidebar',
  title: 'Toggle Sidebar',
  shortcut: const SingleActivator(
    LogicalKeyboardKey.keyB,
    meta: true,
    alt: true,
  ),
);

Each of these items appears in the native macOS menu, alongside the standard items that macOS provides. Your users see a complete, professional menu structure without you writing hundreds of lines of code.

The Technical Magic: Behind the scenes, mac_menu_bar finds the existing NSMenu object (the native macOS menu) and adds your NSMenuItem to it. It's direct manipulation of native macOS UI components, which means:

  • Zero Flutter rendering overhead

  • Perfect native appearance

  • Full accessibility support

  • System-level keyboard shortcut handling

Feature 2: Standard Menu Action Interception

The Promise: Hook into Cut, Copy, Paste, and Select All to customize their behavior.

Why It Matters: This unlocks professional-level features like custom clipboard formats, smart pasting, analytics, and validation.

Real Example: Rich Text Editor

MacMenuBar.onPaste(() async {
      debugPrint('Paste menu item selected');
      return false;
    });
    MacMenuBar.onSelectAll(() async {
      debugPrint('Select all selected');
      return false;
    });
    MacMenuBar.onCut(() async {
      debugPrint('Cut menu item selected');
      return false;
    });
    MacMenuBar.onCopy(() async {
      debugPrint('Copy menu item selected');
      return false;
    });

The Power Move: Notice how we return true when we handle the action, and false when we want the system to handle it? This is brilliant because:

  • You have full control when you need it

  • You defer to the system when you don't

  • Users get consistent behavior

  • You can make runtime decisions about handling

Feature 3: Unlimited Submenu Nesting

The Promise: Create complex menu hierarchies with any level of nesting.

Why It Matters: Professional apps need organized, discoverable features.

Real Example: Developer Tools Menu

Future<void> _setupDevToolsMenu() async {
  // Create main Developer menu
  await MacMenuBar.addSubmenu(
    parentMenuId: 'main',
    submenuId: 'developer',
    title: 'Developer',
  );

  // Add direct items
  await MacMenuBar.addMenuItem(
    menuId: 'developer',
    itemId: 'toggle_console',
    title: 'Toggle Console',
    shortcut: const SingleActivator(
      LogicalKeyboardKey.keyJ,
      meta: true,
      alt: true,
    ),
  );

  // Create Debug submenu
  await MacMenuBar.addSubmenu(
    parentMenuId: 'developer',
    submenuId: 'debug',
    title: 'Debug',
  );

  await MacMenuBar.addMenuItem(
    menuId: 'debug',
    itemId: 'start_debug',
    title: 'Start Debugging',
    shortcut: const SingleActivator(LogicalKeyboardKey.f5),
  );

  await MacMenuBar.addMenuItem(
    menuId: 'debug',
    itemId: 'step_over',
    title: 'Step Over',
    shortcut: const SingleActivator(LogicalKeyboardKey.f10),
  );

  // Create Profiling submenu under Debug
  await MacMenuBar.addSubmenu(
    parentMenuId: 'debug',
    submenuId: 'profiling',
    title: 'Profiling',
  );

  await MacMenuBar.addMenuItem(
    menuId: 'profiling',
    itemId: 'start_cpu_profile',
    title: 'Start CPU Profiling',
  );

  await MacMenuBar.addMenuItem(
    menuId: 'profiling',
    itemId: 'start_memory_profile',
    title: 'Start Memory Profiling',
  );

  // Create Test submenu
  await MacMenuBar.addSubmenu(
    parentMenuId: 'developer',
    submenuId: 'testing',
    title: 'Testing',
  );

  await MacMenuBar.addMenuItem(
    menuId: 'testing',
    itemId: 'run_all_tests',
    title: 'Run All Tests',
    shortcut: const SingleActivator(
      LogicalKeyboardKey.keyT,
      meta: true,
      shift: true,
    ),
  );

  // Even deeper nesting - Unit Tests under Testing
  await MacMenuBar.addSubmenu(
    parentMenuId: 'testing',
    submenuId: 'unit_tests',
    title: 'Unit Tests',
  );

  await MacMenuBar.addMenuItem(
    menuId: 'unit_tests',
    itemId: 'run_widget_tests',
    title: 'Run Widget Tests',
  );
}

Result: A professional, organized menu structure:

Developer
├── Toggle Console          Cmd+Alt+J
├── Debug
│   ├── Start Debugging     F5
│   ├── Step Over          F10
│   └── Profiling
│       ├── Start CPU Profiling
│       └── Start Memory Profiling
└── Testing
    ├── Run All Tests       Cmd+Shift+T
    └── Unit Tests
        └── Run Widget Tests

Why This Matters: Complex applications need organized features. Good menu organization improves discoverability and user productivity. Mac users expect this level of organization in professional apps.

Community and Support

The plugin is:

  • Well-documented: Comprehensive README with examples

  • Thoroughly tested: Full test coverage with unit and integration tests

  • Production-ready: Used in real-world applications

  • Actively maintained: Regular updates and bug fixes

  • Open source: MIT licensed, contributions welcome

The Bottom Line

If you're building a serious macOS app with Flutter, mac_menu_bar isn't just nice to have – it's essential. It gives you:

  • Native macOS integration that PlatformMenuBar can't match

  • Dynamic menu management without widget rebuilds

  • Standard menu action interception for custom behavior

  • Type-safe keyboard shortcuts with full IDE support

  • Professional menu structures with unlimited nesting

  • Better performance through targeted updates

  • Cleaner code with intuitive APIs

Whether you're building a text editor, database tool, creative app, or productivity software, mac_menu_bar lets you create menu experiences that feel truly native to macOS – because they are native.

Your users will notice the difference. Your codebase will thank you. And you'll wonder how you ever managed without it.

Get Started Today

flutter pub add mac_menu_bar

Then check out the documentation and start building menus the way they were meant to be built on macOS. If you want to contribute checkout the repository here.

Your Flutter app deserves menus that feel at home on the Mac. mac_menu_bar makes it happen.


Have you used mac_menu_bar in your project? Share your experience in the comments below!

More from this blog