Menu
iOS 26's pull-down menu: a glass list of actions that grows out of the button that opened it. Choosing a row runs it and closes the menu.
GlassMenuAnchor · GlassMenuItem · GlassMenuController · kGlassMenuRadius · kGlassMenuRowHeight · kGlassMenuWidth · Source
GlassMenuAnchor wraps the widget that opens a menu, usually a "…" GlassButton, and shows a
glass menu of GlassMenuItems when you call open() on the controller its builder is given. The menu grows out of
the button the way iOS 26's does: from the button's own capsule to the menu's size, over the button rather than
beside it. The button is hidden while the menu is open, because the menu is what it turned into.
Choosing a row runs its action and closes the menu. A tap anywhere outside closes it too. There is no dim behind a menu.
When to use
- A "more" (…) button with a handful of actions on an item: rename, duplicate, share, delete.
- A short list of one-shot choices from a button, such as "Sort by".
- Not for controls you adjust in place, such as switches or sliders. Those belong in a popover, which stays open.
- Not for a single confirmation. That's an alert.
Usage
GlassMenuAnchor(
items: <GlassMenuItem>[
GlassMenuItem(label: 'Rename', icon: const Icon(Icons.edit_outlined), onPressed: rename),
GlassMenuItem(label: 'Duplicate', icon: const Icon(Icons.copy), onPressed: duplicate),
GlassMenuItem(label: 'Delete', icon: const Icon(Icons.delete_outline), isDestructive: true, onPressed: delete),
],
builder: (BuildContext context, GlassMenuController menu) => GlassButton(
onPressed: menu.open,
semanticLabel: 'More actions',
padding: EdgeInsets.zero,
child: const Icon(Icons.more_horiz),
),
)
Placement
- The menu opens downward from the button's top when there is room below, and upward from its bottom when not. When it opens upward, the items are reversed, so the first one stays nearest your finger.
- It lines up with the button on the side of the screen the button is on, and stays at least 8 px inside the screen.
- It's built in the nearest
Overlay, so it stands over everything on the page, bars included.
Items
- Each row is kGlassMenuRowHeight (42) high, with the icon in a 48 px column before the label. Labels are one line and end with an ellipsis.
isDestructivedraws the row in iOS red.onPressed: nulldisables the row and dims it to 30%.- The menu is kGlassMenuWidth (250)
wide unless you pass
width, with corners of kGlassMenuRadius (31.5).
Opening from code
Pass your own GlassMenuController to open or close the menu from anywhere: open(), close() and isOpen. A
controller drives one anchor at a time.
Setup and accessibility
- The GlassHost must be above the overlay the menu is built in. With the host in
MaterialApp(builder: ...), that's true everywhere. - Give an icon-only button a
semanticLabel. The area around the open menu is announced withbarrierLabel.
Code
import 'package:flutter/material.dart';
import 'package:g1455/g1455.dart';
/// A note's header with a "…" menu of actions.
/// Assumes a GlassHost above the navigator, e.g. in MaterialApp.builder.
class NoteHeader extends StatefulWidget {
const NoteHeader({required this.title, super.key});
final String title;
@override
State<NoteHeader> createState() => _NoteHeaderState();
}
class _NoteHeaderState extends State<NoteHeader> {
bool _pinned = false;
void _toast(String what) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(what)));
@override
Widget build(BuildContext context) => Row(
children: <Widget>[
Expanded(child: Text(widget.title, style: Theme.of(context).textTheme.headlineSmall)),
GlassMenuAnchor(
width: 220,
items: <GlassMenuItem>[
GlassMenuItem(
label: _pinned ? 'Unpin' : 'Pin',
icon: const Icon(Icons.push_pin_outlined, size: 20),
onPressed: () => setState(() => _pinned = !_pinned),
),
GlassMenuItem(
label: 'Duplicate',
icon: const Icon(Icons.copy, size: 20),
onPressed: () => _toast('Duplicated'),
),
const GlassMenuItem(label: 'Move to…', icon: Icon(Icons.folder_outlined, size: 20)), // disabled
GlassMenuItem(
label: 'Delete',
icon: const Icon(Icons.delete_outline, size: 20),
isDestructive: true,
onPressed: () => _toast('Deleted'),
),
],
builder: (BuildContext context, GlassMenuController menu) => GlassButton(
onPressed: menu.open,
semanticLabel: 'More actions',
padding: EdgeInsets.zero,
child: const Icon(Icons.more_horiz),
),
),
],
);
}API
GlassMenuAnchor
| Parameter | Type | Default | Description |
|---|---|---|---|
items |
List<GlassMenuItem> |
required | The menu's rows, top to bottom. |
builder |
Widget Function(BuildContext, GlassMenuController) |
required | Builds the anchor, such as a button, and gets the controller that opens the menu. |
controller |
GlassMenuController? |
null |
Your own controller, to open or close the menu from outside. Null makes an internal one. |
finish |
GlassFinish? |
null |
The glass. Null takes the theme's. |
width |
double |
kGlassMenuWidth (250) |
The menu's width. |
barrierLabel |
String |
'Dismiss' |
What a screen reader says for the area around the open menu. |
GlassMenuItem
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
String |
required | The row's text, one line. |
icon |
Widget? |
null |
Drawn before the label. |
onPressed |
VoidCallback? |
null |
The action. Null disables the row. The menu closes after it runs. |
isDestructive |
bool |
false |
Draws the row in iOS red. |
GlassMenuController: open(), close(), bool get isOpen.
Constants
| Name | Value | Description |
|---|---|---|
kGlassMenuWidth |
250 |
The menu's default width. |
kGlassMenuRowHeight |
42 |
Each row's height. |
kGlassMenuRadius |
31.5 |
The menu's corner radius, and the popover's default. |