base_menu 0.1.4 copy "base_menu: ^0.1.4" to clipboard
base_menu: ^0.1.4 copied to clipboard

Composable widgets for building menu systems.

Base Menu #

Composable widgets for building menu systems in Flutter.

Deploy Platform License: MIT

Live Gallery · Floogle Docs Demo · API Reference

Sequoia Menu Bar Demo

Features #

  • Completely headless: no pixels are painted by this library
  • Keyboard support that follows the WAI-ARIA Menu and Menubar Pattern
  • Menu aim assist (safe triangles)
  • A robust positioning algorithm with support for custom layout delegates
  • Built on the MenuController API

Architecture #

Base Menu provides a small set of primitive widgets that control keyboard traversal, focus routing, and menu positioning. These widgets can be composed to create a wide variety of menu systems, from simple context menus to complex multi-level menu bars.

Core Components #

Component Description Visual
BaseMenu A single-layer menu overlay or context trigger. BaseMenu
BaseSubmenu A BaseMenu specialized for nested menus. Coordinates cross-axis keyboard navigation and hover traversal. Uses a BaseMenuItem as its anchor. BaseSubmenu
BaseMenuBar An inline grouping layer that coordinates keyboard and focus routing for a set of menu buttons. BaseMenuBar
BaseMenuItem A primary control that adds hover-to-focus and close-on-activate actions to a BaseControl. BaseMenuItem
BaseMenuPanel A layout container for a set of menu items. BaseMenuPanel

Getting started #

Add the package to your pubspec.yaml and import it into your project:

import 'package:base_menu/base_menu.dart';

Customization #

Controls #

Full guide

Add custom styling to menu items by using state selectors in a BaseControl or BaseMenuItem.

// Implement styling for a menu item using the state selectors from BaseMenuItem.
class StyledLabel extends StatelessWidget {
  const StyledLabel({super.key, required this.child});
  final Widget child;

  static const WidgetStateProperty<BoxDecoration> decoration = WidgetStateProperty.fromMap({
    WidgetState.pressed: BoxDecoration(color: Color(0xFF2B4678)),
    WidgetState.focused: BoxDecoration(color: Color(0xFF445E91)),
    WidgetState.any: BoxDecoration(color: Color(0x00000000)),
  });

  static const WidgetStateProperty<TextStyle> textStyle = WidgetStateProperty.fromMap({
    WidgetState.disabled: TextStyle(color: Color(0xFF74777F)),
    WidgetState.pressed: TextStyle(color: Color(0xFFFFFFFF)),
    WidgetState.focused: TextStyle(color: Color(0xFFFFFFFF)),
    WidgetState.any: TextStyle(color: Color(0xFF1A1B20)),
  });

  @override
  Widget build(BuildContext context) {
    final states = BaseMenuItem.statesOf(context);
    return DecoratedBox(
      decoration: decoration.resolve(states),
      child: DefaultTextStyle.merge(
        style: textStyle.resolve(states),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
          child: Align(alignment: AlignmentDirectional.centerStart, child: child),
        ),
      ),
    );
  }
}

// Usage:
class CustomMenuItem extends StatelessWidget {
  const CustomMenuItem({super.key, required this.child, this.onPressed});
  final Widget child;
  final VoidCallback? onPressed;

  @override
  Widget build(BuildContext context) {
    return BaseMenuItem(
      onPressed: onPressed,
      child: StyledLabel(child: child),
    );
  }
}

Panel #

Wrap a BaseMenuPanel in a DecoratedBox to apply a background color, border, or shadow:

class StyledMenuPanel extends StatelessWidget {
  const StyledMenuPanel({super.key, required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: const BoxDecoration(
        color: Color(0xffffffff),
        borderRadius: BorderRadius.all(Radius.circular(4)),
        boxShadow: [
          BoxShadow(color: Color(0x18000000), blurRadius: 8, offset: Offset(0, 4)),
          BoxShadow(color: Color(0x0D000000), blurRadius: 4, offset: Offset(0, 2)),
        ],
      ),
      child: child,
    );
  }
}

// Usage:
BaseMenu(
  menu: StyledMenuPanel(
    child: BaseMenuPanel(
      children: [
        const MenuItem(label: 'Undo'),
        const MenuItem(label: 'Redo'),
      ],
    ),
  ),
  // ...
);

Positioning #

Full guide

Use DefaultMenuPositioningDelegate to configure the menu's position relative to its anchor:

BaseMenu(
  positionDelegate: DefaultMenuPositioningDelegate(
    // Attach the bottom-center of the anchor to the top-center of the menu
    anchorAttachment: Alignment.bottomCenter,
    menuAttachment: Alignment.topCenter,
    // Add a 4-pixel vertical gap between the anchor and the menu
    offset: Offset(0, 4),
    // Match the vertical padding of the menu panel (6 pixels).
    padding: EdgeInsets.symmetric(vertical: 6.0),
  ),
  // ...
)

Positioned Menu

Aim assist #

Full guide

Disabled Enabled
Aim Assist Disabled Aim Assist Enabled

To enable aim assist for a single menu, set the enableAimAssist property of the menu's positioning delegate to true.

To control the behavior of aim assist for a subtree, wrap the subtree with MenuAimScope and set the enable property to true. All descendant menus will inherit the enable value unless a descendant menu's positioning delegate overrides it.

// Enable aim assist for a subtree of menus.
MenuAimScope(
  enable: true,
  child: Column(
    children: [
      // Aim assist is enabled for this menu and its descendants.
      BaseSubmenu(
        controller: controllerOne,
        menu: ExamplePanel(),
        child: Text('File'),
      ),
      BaseSubmenu(
        controller: controllerTwo,
        // Aim assist is disabled for this menu, but not its descendants.
        positionDelegate: DefaultMenuPositioningDelegate(
          enableAimAssist: false,
        ),
        menu: ExamplePanel(),
        child: Text('Edit'),
      ),
    ],
  ),
)

Contributing #

Contributions are welcome! Please open an issue or submit a pull request on GitHub. I'm especially interested in feedback on accessibility, API ergonomics, and edge cases in keyboard traversal and menu positioning logic.

0
likes
150
points
86
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Composable widgets for building menu systems.

Homepage
Repository (GitHub)
View/report issues

Topics

#menus #ui

License

MIT (license)

Dependencies

flutter

More

Packages that depend on base_menu