g1455

Toolbar

Several toolbar actions in one glass capsule, the way iOS 26 groups toolbar items. One surface for any number of actions, so it costs far less than a row of buttons.

iOS 26 doesn't put a glass behind each toolbar button. It groups adjacent items into one glass shape: a 48 px circle for an item on its own, and a capsule for several. GlassButtonGroup does the same. You give it a list of GlassToolbarItems (an icon, an action and a label for screen readers), and it draws them in one glass surface.

Pressing an item brightens just its cell, the same way a GlassButton brightens: the finish's rim colour is added over the cell, clipped to the capsule. Icons are themed at 22 px in the label colour the theme picks for the glass.

When to use

  • Icon actions in a toolbar or over content: undo and redo, share, like, delete, previous and next.
  • Instead of a row of GlassButtons whenever the actions sit next to each other.
  • Not for text labels. The group is icon-first and its size is fixed. For a labelled action, use a GlassButton.
  • Not for choosing one of several values. That's a segmented control.

Usage

Row(
  children: <Widget>[
    GlassButtonGroup(
      items: <GlassToolbarItem>[
        GlassToolbarItem(icon: const Icon(Icons.undo), label: 'Undo', onPressed: canUndo ? undo : null),
        GlassToolbarItem(icon: const Icon(Icons.redo), label: 'Redo', onPressed: canRedo ? redo : null),
      ],
    ),
    const Spacer(),
    GlassButtonGroup(
      items: <GlassToolbarItem>[
        GlassToolbarItem(icon: const Icon(Icons.ios_share), label: 'Share', onPressed: share),
      ],
    ),
  ],
)

Why one surface

Glass is paid for per surface, and the overhead grows faster than the number of surfaces. Three GlassButtons are three surfaces; a group of three is one. Grouping is how Apple draws it, and it's also the cheap way to build it. See Performance.

Layout

  • The size is fixed: kGlassToolbarHeight (48) high, and 48 wide for one item or kGlassToolbarItemWidth (53) per item for more. Don't stretch it; place groups with a Row and Spacers.
  • Four items are 212 px wide. On a narrow phone, a 4-item group plus two circles don't fit in 320 px: drop an action or move it into a menu.
  • Groups float over the content on their own, as in iOS. Putting them inside a GlassBar works, but it is glass on glass and adds a capture level.

Accessibility

  • Always set label. It is what a screen reader says for the item; the icon alone says nothing.
  • onPressed: null disables an item: its icon drops to 30% and it is announced as disabled.

Warning

Don't wrap a group in Opacity, ColorFilter or ImageFilter. The pressed highlight is added onto what's below, and those widgets break it. RepaintBoundary is fine.

Code

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

/// A photo viewer's bottom toolbar: share alone, three actions grouped,
/// and delete alone. Three glass surfaces for five actions.
/// Assumes a GlassHost above, e.g. in MaterialApp.builder.
class PhotoViewer extends StatefulWidget {
  const PhotoViewer({required this.photo, super.key});

  final ImageProvider photo;

  @override
  State<PhotoViewer> createState() => _PhotoViewerState();
}

class _PhotoViewerState extends State<PhotoViewer> {
  bool _liked = false;

  void _toast(String what) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(what)));

  @override
  Widget build(BuildContext context) => Stack(
    children: <Widget>[
      Positioned.fill(child: Image(image: widget.photo, fit: BoxFit.cover)),
      Positioned(
        left: 16,
        right: 16,
        bottom: 16 + MediaQuery.paddingOf(context).bottom,
        child: Row(
          children: <Widget>[
            GlassButtonGroup(
              items: <GlassToolbarItem>[
                GlassToolbarItem(icon: const Icon(Icons.ios_share), label: 'Share', onPressed: () => _toast('Share')),
              ],
            ),
            const Spacer(),
            GlassButtonGroup(
              items: <GlassToolbarItem>[
                GlassToolbarItem(
                  icon: Icon(_liked ? Icons.favorite : Icons.favorite_border),
                  label: _liked ? 'Unlike' : 'Like',
                  onPressed: () => setState(() => _liked = !_liked),
                ),
                GlassToolbarItem(icon: const Icon(Icons.info_outline), label: 'Info', onPressed: () => _toast('Info')),
                GlassToolbarItem(icon: const Icon(Icons.tune), label: 'Edit', onPressed: () => _toast('Edit')),
              ],
            ),
            const Spacer(),
            GlassButtonGroup(
              items: <GlassToolbarItem>[
                GlassToolbarItem(
                  icon: const Icon(Icons.delete_outline),
                  label: 'Delete',
                  onPressed: () => _toast('Delete'),
                ),
              ],
            ),
          ],
        ),
      ),
    ],
  );
}

API

GlassButtonGroup

Parameter Type Default Description
items List<GlassToolbarItem> required The actions, left to right. Must not be empty.
finish GlassFinish? null The glass. Null takes the theme's.
pressedOverlay Color? null Added over a held item's cell. Null takes the finish's rim colour.

GlassToolbarItem

Parameter Type Default Description
icon Widget required The icon, themed at 22 px in the label colour.
onPressed VoidCallback? required The action. Null disables the item (icon at 30%).
label String? null What a screen reader says. Always set it.

Constants

Name Value Description
kGlassToolbarHeight 48 The group's height, and the size of a one-item circle.
kGlassToolbarItemWidth 53 Each item's width in a group of two or more.