RemixRemix
Navigation

Menu

A dropdown menu component for displaying a list of actions or options

Open catalog
Starting Flutter example…
Live Flutter from this checkout’s component catalog. Examples include Fortal styling; the source tab shows the catalog code, not a standalone application.

A dropdown menu component for displaying actions or options in an overlay.

When to use this

  • Action menus: Show a list of available actions for a selected item
  • Context menus: Provide contextual options based on user selection
  • Navigation: Create dropdown navigation menus
  • Settings: Display configuration options in a compact dropdown

Basic implementation

Basic implementation

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

class MenuExample extends StatefulWidget {
  const MenuExample({super.key});

  @override
  State<MenuExample> createState() => _MenuExampleState();
}

class _MenuExampleState extends State<MenuExample> {
  final controller = MenuController();

  @override
  Widget build(BuildContext context) {
    return RemixMenu<String>(
      trigger: const RemixMenuTrigger(label: 'Open Menu'),
      items: [
        RemixMenuItem(
          value: 'History',
          leadingIcon: Icons.history,
          label: 'History',
        ),
        RemixMenuItem(
          value: 'Settings',
          leadingIcon: Icons.settings,
          label: 'Settings',
        ),
        const RemixMenuDivider(),
        RemixMenuItem(
          value: 'Logout',
          leadingIcon: Icons.logout,
          label: 'Logout',
          style: MenuItemStyler().onHovered(
            MenuItemStyler()
                .color(Colors.redAccent.withValues(alpha: 0.05))
                .label(TextStyler().color(Colors.redAccent))
                .leadingIcon(IconStyler().color(Colors.redAccent)),
          ),
        ),
      ],
      positioning: const OverlayPositionConfig(
        sideOffset: 8,
        side: OverlaySide.bottom,
        alignment: OverlayAlignment.center,
      ),
      style: menuStyle,
      onSelected: (value) {
        debugPrint('Selected: $value');
      },
      controller: controller,
    );
  }

  MenuStyler get menuStyle {
    return MenuStyler()
        .trigger(
          MenuTriggerStyler()
              .padding(EdgeInsetsMix.symmetric(horizontal: 14))
              .decoration(
                BoxDecorationMix()
                    .color(Colors.white)
                    .borderRadius(BorderRadiusMix.all(const Radius.circular(12)))
                    .border(BorderMix.all(BorderSideMix(color: Colors.blueGrey.shade100)))
                    .boxShadow([
                  BoxShadowMix(
                    color: Colors.blueGrey.withValues(alpha: 0.1),
                    blurRadius: 3,
                    offset: const Offset(0, 3),
                  ),
                ]),
              )
              .constraints(BoxConstraintsMix(minHeight: 40))
              .label(
                TextStyler()
                    .color(Colors.blueGrey.shade700)
                    .fontWeight(FontWeight.w400),
              ),
        )
        .overlay(
          FlexBoxStyler(
            padding: EdgeInsetsMix.all(12),
            decoration: BoxDecorationMix(
              color: Colors.white,
              borderRadius: BorderRadiusMix.all(const Radius.circular(12)),
              border: BorderMix.all(BorderSideMix(color: Colors.blueGrey.shade100)),
              boxShadow: [
                BoxShadowMix(
                  color: Colors.blueGrey.withValues(alpha: 0.1),
                  blurRadius: 3,
                  offset: const Offset(0, 3),
                ),
              ],
            ),
          ),
        )
        .item(menuItemStyle);
  }

  MenuItemStyler get menuItemStyle {
    return MenuItemStyler()
        .padding(.all(6))
        .leadingIcon(IconStyler().size(20).color(Colors.blueGrey.shade800))
        .spacing(8)
        .borderRadius(.all(const Radius.circular(8)))
        .label(TextStyler().color(Colors.blueGrey.shade800))
        .onHovered(MenuItemStyler().color(Colors.blueGrey.shade50));
  }
}

Custom trigger content

RemixMenuTrigger is a configuration object, not a widget. The same object is the trigger for FortalMenu; there is no separate Fortal trigger type.

Use the unnamed constructor for the standard label/icon surface. Use RemixMenuTrigger.builder when the trigger needs richer visual content such as an avatar or profile row. The builder receives the current menu state and the already styled default trigger as defaultTrigger. It may wrap that child or replace it.

Builder output must be non-interactive visual content. Returning a FortalButton, FortalIconButton, or any other nested interactive control creates duplicate semantics and a second tap target. NakedMenu already owns tapping, focus, keyboard behavior, and expanded state.

Builder-backed avatar trigger

import 'package:flutter/material.dart';
import 'package:remix/remix.dart';
import 'ui/ui.dart';

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

  @override
  Widget build(BuildContext context) {
    return FortalMenu<String>(
      trigger: RemixMenuTrigger.builder(
        label: 'Account menu',
        builder: (context, state, defaultTrigger) =>
            const FortalAvatar(label: 'LF'),
      ),
      items: const [
        RemixMenuItem(value: 'profile', label: 'View profile'),
        RemixMenuItem(value: 'preferences', label: 'Preferences'),
        RemixMenuDivider(),
        RemixMenuItem(value: 'signout', label: 'Sign out'),
      ],
      onSelected: (value) => debugPrint(value),
    );
  }
}

Checkboxes, radio groups, and submenus

Checkboxes and radio groups are controlled. Keep their current value in your widget state and update that state from onChanged. Submenus can contain any menu item data, including another submenu.

Controlled compound items

import 'package:flutter/material.dart';
import 'package:remix/remix.dart';
import 'ui/ui.dart';

class CompoundMenuExample extends StatefulWidget {
  const CompoundMenuExample({super.key});

  @override
  State<CompoundMenuExample> createState() => _CompoundMenuExampleState();
}

class _CompoundMenuExampleState extends State<CompoundMenuExample> {
  bool showStatus = true;
  String density = 'comfortable';

  @override
  Widget build(BuildContext context) {
    return FortalMenu<String>.soft(
      trigger: const RemixMenuTrigger(label: 'View options'),
      onSelected: (value) => debugPrint('Root selected: $value'),
      items: [
        RemixMenuCheckboxItem(
          value: 'show-status',
          label: 'Show status',
          checked: showStatus,
          closeOnActivate: false,
          onChanged: (next) => setState(() => showStatus = next),
        ),
        RemixMenuRadioGroup(
          value: density,
          onChanged: (next) => setState(() => density = next),
          items: const [
            RemixMenuRadioItem(
              value: 'compact',
              label: 'Compact',
              closeOnActivate: false,
            ),
            RemixMenuRadioItem(
              value: 'comfortable',
              label: 'Comfortable',
              closeOnActivate: false,
            ),
          ],
        ),
        const RemixMenuDivider(),
        const RemixMenuSubmenu(
          label: 'Share',
          items: [
            RemixMenuItem(value: 'copy-link', label: 'Copy link'),
            RemixMenuSubmenu(
              label: 'More',
              items: [
                RemixMenuItem(value: 'email', label: 'Email'),
              ],
            ),
          ],
        ),
      ],
    );
  }
}

RemixMenuCheckboxItem.checked and RemixMenuRadioGroup.value are never changed internally. On activation, RemixMenu.onSelected runs first, followed by the checkbox or radio-group onChanged. Selecting the already-selected radio item re-emits its value. A checkbox or radio item is disabled when neither its local callback nor the root onSelected callback is supplied.

closeOnActivate defaults to true. Set it to false for settings that should remain visible while they are changed. Submenus open after a 100 ms hover delay by default. The directional forward-arrow opens a focused submenu, the reverse arrow closes it, and Escape closes it and restores focus to its trigger. These directions are mirrored automatically in right-to-left layouts.

KeyBehavior
Down / Up arrowMove focus between enabled items
Enter / SpaceActivate the focused item
Right arrow in LTR / Left arrow in RTLOpen the focused submenu and focus its first item
Left arrow in LTR / Right arrow in RTLClose the current submenu and focus its trigger
EscapeClose the current menu level and restore trigger focus

The root items list, each radio group's items, and every submenu's items are copied to immutable snapshots when their panel is built. Rebuild the menu after changing a source list; do not rely on mutating a visible panel in place.

For dynamic menus, including lists that are filtered, have items inserted, or are reordered, supply a stable caller-owned key: on each ordinary item, checkbox item, radio group, radio item, submenu, and divider. Build keys from application-owned item identity and keep them stable across rebuilds. Item keys are not derived automatically from labels or values.

Compound-item styling

Compound rows use the same MenuItemStyler as ordinary rows, including hovered, focused, pressed, disabled, and selected-state variants. An open submenu trigger contributes WidgetState.selected.

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

final sharedItemStyle = MenuItemStyler()
    .padding(.horizontal(12))
    .spacing(8)
    .onSelected(.color(Colors.indigo.withValues(alpha: 0.08)));

final choiceItemStyle = MenuItemStyler().indicator(
  IconStyler().size(9).color(Colors.indigo),
);

final appMenuStyle = MenuStyler()
    .item(sharedItemStyle)
    .checkboxItem(choiceItemStyle)
    .radioItem(choiceItemStyle)
    .submenuItem(
      MenuItemStyler().trailingIcon(IconStyler().color(Colors.indigo)),
    );

item is the shared foundation for every actionable row. The semantic checkboxItem, radioItem, and submenuItem styles merge after it, and an individual item's style merges last. RemixMenuRadioGroup is behavioral and does not add another styling layer. Use the normal MenuItemStyler flex, padding, and spacing methods for layout.

The renderer reserves one leading choice slot for every row in a panel when that panel directly contains a checkbox or radio group. Nested submenu panels decide this independently. A custom trailingIcon replaces the default directional submenu chevron.

Fortal widgets

The application-owned Fortal preset provides a themed widget for this component:

Fortal variants

import 'package:flutter/material.dart';
import 'package:remix/remix.dart';
import 'ui/ui.dart';

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

  @override
  Widget build(BuildContext context) {
    final choiceItemStyle = MenuItemStyler().indicator(
      IconStyler().color(Colors.indigo),
    );
    final customStyle = MenuStyler()
        .checkboxItem(choiceItemStyle)
        .radioItem(choiceItemStyle);
    final items = <RemixMenuItemData<String>>[
      const RemixMenuItem(
        value: 'edit',
        label: 'Edit',
        leadingIcon: Icons.edit,
      ),
      const RemixMenuCheckboxItem(
        value: 'show-status',
        label: 'Show status',
        checked: true,
      ),
      const RemixMenuRadioGroup(
        value: 'comfortable',
        items: [
          RemixMenuRadioItem(value: 'compact', label: 'Compact'),
          RemixMenuRadioItem(value: 'comfortable', label: 'Comfortable'),
        ],
      ),
    ];

    return Row(
      spacing: 16,
      children: [
        RemixMenu<String>(
          style: fortalMenuStyle(
            variant: FortalMenuVariant.solid,
          ).merge(customStyle),
          trigger: const RemixMenuTrigger(label: 'Solid', icon: Icons.menu),
          items: items,
          onSelected: (value) => debugPrint(value),
        ),
        RemixMenu<String>(
          style: fortalMenuStyle(
            variant: FortalMenuVariant.soft,
          ).merge(customStyle),
          trigger: const RemixMenuTrigger(label: 'Soft', icon: Icons.menu),
          items: items,
          onSelected: (value) => debugPrint(value),
        ),
      ],
    );
  }
}

The generated FortalMenu, FortalMenu.solid, and FortalMenu.soft constructors apply only the selected recipe. For reusable menu-wide customization, pass fortalMenuStyle(...).merge(customStyle) to RemixMenu.style; the custom fields merge after the recipe.

Fortal uses the pinned check and submenu-chevron paths, colors, sizes, and states. Their placement uses Remix's standard leading flex slot and item spacing rather than Radix's absolute CSS offsets, so small indicator and trailing-gap differences are the documented visual tolerance.

See the fortalMenuStyle source code for all available options.

Constructor

Constructor

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

RemixMenu<T> remixMenuConstructor<T>({
  Key? key,
  required RemixMenuTrigger trigger,
  required List<RemixMenuItemData<T>> items,
  MenuController? controller,
  ValueChanged<T>? onSelected,
  VoidCallback? onOpen,
  VoidCallback? onClose,
  VoidCallback? onCanceled,
  RawMenuAnchorOpenRequestedCallback? onOpenRequested,
  RawMenuAnchorCloseRequestedCallback? onCloseRequested,
  bool consumeOutsideTaps = true,
  bool useRootOverlay = false,
  bool closeOnClickOutside = true,
  FocusNode? triggerFocusNode,
  OverlayPositionConfig positioning = const OverlayPositionConfig(),
  String? semanticLabel,
  bool excludeSemantics = false,
  MenuStyler style = const MenuStyler.create(),
  MenuSpec? styleSpec,
}) => throw UnimplementedError();

Properties

Widget Properties

key → Key?

Optional. Controls how one widget replaces another widget in the tree.

trigger → RemixMenuTrigger

Required. Configuration for the menu's button, not a widget. Use RemixMenuTrigger(label:, icon:) for the standard surface or RemixMenuTrigger.builder(label:, icon:, builder:) for custom visual content. label is the visible default label and the accessible fallback for a builder-backed trigger. The same object works with FortalMenu.

items → List<RemixMenuItemData<T>>

Required. The list of ordinary items, checkbox items, radio groups, submenus, and dividers.

controller → MenuController?

Optional. Optional controller for programmatic control of the menu state. If not provided, an internal controller will be created automatically.

onSelected → ValueChanged<T>?

Optional. Called when an item is selected.

onOpen → VoidCallback?

Optional. Called when the menu opens.

onClose → VoidCallback?

Optional. Called when the menu closes.

onCanceled → VoidCallback?

Optional. Called when the menu closes without a selection.

onOpenRequested → RawMenuAnchorOpenRequestedCallback?

Optional. Open/close interceptors (for example, to drive animations).

onCloseRequested → RawMenuAnchorCloseRequestedCallback?

Optional.

consumeOutsideTaps → bool

Optional. Whether outside taps on the trigger are consumed.

useRootOverlay → bool

Optional. Whether to target the root overlay instead of the nearest ancestor.

closeOnClickOutside → bool

Optional. Whether taps outside the overlay close the menu.

triggerFocusNode → FocusNode?

Optional. Optional focus node for the trigger.

positioning → OverlayPositionConfig

Optional. Overlay positioning configuration.

semanticLabel → String?

Optional. The accessible label for the menu trigger.

excludeSemantics → bool

Optional. Whether to hide the menu trigger from the semantics tree. Defaults to false. This applies to the trigger only — the overlay is mounted separately, so an open menu keeps announcing its items.

style → MenuStyler

Optional. The style configuration for the menu.

styleSpec → MenuSpec?

Optional. A raw resolved style spec that bypasses fluent style resolution. For semantic item fields, null inherits the raw item spec and a supplied value completely replaces it; per-item fluent styles remain bypassed.

trigger(MenuTriggerStyler value)

Configures the trigger button style.

overlay(FlexBoxStyler value)

Configures the menu overlay container style.

item(MenuItemStyler value)

Configures the shared default for every actionable menu row.

checkboxItem(MenuItemStyler value)

Configures the menu-wide checkbox-row override after item.

radioItem(MenuItemStyler value)

Configures the menu-wide radio-row override after item.

Configures the menu-wide submenu-trigger override after item.

divider(DividerStyler value)

Configures divider style.

animate(AnimationConfig value)

Configures implicit animation for style transitions.

variants(List<VariantStyle<MenuSpec>> value)

Sets style variants.

wrap(WidgetModifierConfig value)

Applies widget modifiers such as clipping, opacity, or scaling.

modifier(WidgetModifierConfig value)

Sets the widget modifier.

call<T>({..., String? semanticLabel, bool excludeSemantics = false})

Creates a RemixMenu widget with this style applied.

container(FlexBoxStyler value)

Configures the trigger container style.

label(TextStyler value)

Configures the trigger label style.

icon(IconStyler value)

Configures the trigger icon style.

animate(AnimationConfig value)

Configures implicit animation for trigger style transitions.

variants(List<VariantStyle<MenuTriggerSpec>> value)

Sets trigger style variants.

wrap(WidgetModifierConfig value)

Applies widget modifiers such as clipping, opacity, or scaling.

modifier(WidgetModifierConfig value)

Sets the trigger widget modifier.

labelStyle(TextStyleMix value)

Sets label/text style using TextStyleMix directly

labelColor(Color value)

Sets label/text color

labelFontSize(double value)

Sets label/text font size

labelFontWeight(FontWeight value)

Sets label/text font weight

labelFontStyle(FontStyle value)

Sets label/text font style (italic/normal)

labelLetterSpacing(double value)

Sets label/text letter spacing

labelDecoration(TextDecoration value)

Sets label/text decoration (underline, strikethrough, etc.)

labelFontFamily(String value)

Sets label/text font family

labelHeight(double value)

Sets label/text line height

labelWordSpacing(double value)

Sets label/text word spacing

labelDecorationColor(Color value)

Sets label/text decoration color

iconColor(Color value)

Sets icon color

iconSize(double value)

Sets icon size

iconOpacity(double value)

Sets icon opacity

iconWeight(double value)

Sets icon weight (useful for variable icons like Material Symbols)

iconGrade(double value)

Sets icon grade (useful for Material Icons)

iconFill(double value)

Sets icon fill (useful for Material Icons filled variants)

iconOpticalSize(double value)

Sets icon optical size (useful for Material Icons)

iconBlendMode(BlendMode value)

Sets icon blend mode

iconTextDirection(TextDirection value)

Sets icon text direction

iconShadows(List<ShadowMix> value)

Sets icon shadows

iconShadow(ShadowMix value)

Sets single icon shadow

container(FlexBoxStyler value)

Configures the item container style.

label(TextStyler value)

Configures the label text style using a TextStyler.

leadingIcon(IconStyler value)

Configures the leading icon style.

trailingIcon(IconStyler value)

Configures the trailing icon style.

indicator(IconStyler value)

Configures the checkbox and radio indicator style.

alignment(Alignment value)

Sets container alignment

padding(EdgeInsetsGeometryMix value)

Sets the container padding.

color(Color value)

Sets background color.

size(double width, double height)

Sets the component size.

borderRadius(BorderRadiusGeometryMix radius)

Sets the border radius.

constraints(BoxConstraintsMix value)

Sets size constraints on the component.

decoration(DecorationMix value)

Sets the container decoration.

margin(EdgeInsetsGeometryMix value)

Sets the container margin.

foregroundDecoration(DecorationMix value)

Sets a foreground decoration painted on top of the component.

transform(Matrix4 value, AlignmentGeometry alignment = Alignment.center)

Applies a matrix transformation to the component.

animate(AnimationConfig value)

Configures implicit animation for style transitions.

variants(List<VariantStyle<MenuItemSpec>> value)

Sets item style variants.

wrap(WidgetModifierConfig value)

Applies widget modifiers such as clipping, opacity, or scaling.

modifier(WidgetModifierConfig value)

Sets the item widget modifier.

View page source on GitHub

On this page