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

Composable primitives for building accessible menu systems.

Base Menu #

Composable primitives for building accessible menu systems.

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.

26
likes
160
points
301
downloads

Documentation

API reference

Publisher

verified publisherdavidhicks.dev

Weekly Downloads

Composable primitives for building accessible menu systems.

Homepage
Repository (GitHub)
View/report issues

Topics

#menus #ui

License

MIT (license)

Dependencies

flutter

More

Packages that depend on base_menu