g1455

Morph

One piece of glass that flows to the size of whatever child it holds: a button that becomes a panel, with a liquid neck while it moves.

GlassMorph holds one child on glass. Give it a child of another identity, another type or another Key, and the glass flows to the new child's size while the old content fades out and the new fades in. It's how a button becomes its menu in iOS 26.

You never type a size: the glass measures the child. width and height are overrides that pin one axis.

When to use

  • A control that turns into the panel it opens, in place: a "+" into a list of actions, a pill into a search field.
  • A card whose content changes size, where the glass should follow rather than jump.
  • Not for a menu that floats over the page and closes on a tap outside. Use the menu or the popover, which bring their own overlay and barrier.

Usage

Align(
  alignment: Alignment.topRight,
  child: GlassMorph(
    alignment: Alignment.topRight,
    borderRadius: open ? const BorderRadius.all(Radius.circular(28)) : kGlassCapsule,
    child: open
        ? ActionsPanel(key: const ValueKey<String>('panel'), onDone: close)
        : PlusButton(key: const ValueKey<String>('plus'), onTap: openPanel),
  ),
)

Alignment: what holds still

alignment is the point of the glass that stays put inside the morph's own box while the size changes. The box is placed by the morph's parent, so to hold a corner on the screen, the parent has to hold the same corner: an Align, a Positioned with top and right, the end of a Row. Inside a Center, the glass grows from its centre whatever alignment says. Give the parent and the morph the same alignment, as the demo does.

Identity

The rule is AnimatedSwitcher's. A child of the same type and key is updated in place: no morph, and the glass takes its new size at once. Changing width, height or borderRadius alone does morph. A child swapped back while it is still fading out keeps its state.

Motion

  • GlassMorphMotion.fluid, the default: a spring with a little overshoot.
  • GlassMorphMotion.calm: no overshoot, a little slower, for large panels.
  • Or your own: GlassMorphMotion(duration: ..., bounce: ...), SwiftUI's spring parameters.
  • With reduce motion on, there is no motion: the new child and its size arrive at once.

Cost

  • At rest it's one plain glass surface.
  • While it grows, it's a group of two shapes, and a capture on every frame, because the glass changes size. Wrap a fixed-size ancestor that holds every size the morph takes in a GlassTravel, and the motion is drawn from the proxy already held.
  • spacing: 0 turns the neck off: a plain resize with the cross-fade, and no group even mid-morph.

Accessibility

  • Only the new child is hit and read by a screen reader while the glass moves.
  • Label the controls inside the panel as you would anywhere else.

Code

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

/// A "+" in the corner that flows into a panel of actions.
/// Assumes a GlassHost above, e.g. in MaterialApp.builder.
class NewThing extends StatefulWidget {
  const NewThing({super.key});

  @override
  State<NewThing> createState() => _NewThingState();
}

class _NewThingState extends State<NewThing> {
  bool _open = false;

  @override
  Widget build(BuildContext context) => Align(
    // The parent holds the top-right corner, and so does the glass.
    alignment: Alignment.topRight,
    child: GlassMorph(
      alignment: Alignment.topRight,
      borderRadius: _open ? const BorderRadius.all(Radius.circular(28)) : kGlassCapsule,
      child: _open
          ? Column(
              key: const ValueKey<String>('panel'),
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                for (final String name in <String>['Note', 'List', 'Photo'])
                  SizedBox(
                    width: 200,
                    child: TextButton(onPressed: () => setState(() => _open = false), child: Text(name)),
                  ),
              ],
            )
          : IconButton(
              key: const ValueKey<String>('plus'),
              tooltip: 'New',
              onPressed: () => setState(() => _open = true),
              icon: const Icon(Icons.add),
            ),
    ),
  );
}

API

Parameter Type Default Description
child Widget required What the glass holds and measures. A child of another type or key starts a morph.
alignment AlignmentGeometry Alignment.center The point that holds still in the morph's box. The parent has to hold it on screen.
width double? null Pins the glass's width; the child is laid out at it. Null measures.
height double? null Pins the glass's height; the child is laid out at it. Null measures.
borderRadius BorderRadius 24 all round The corners around this child. kGlassCapsule is a pill at any size.
motion GlassMorphMotion GlassMorphMotion.fluid The spring. GlassMorphMotion.calm has no overshoot.
spacing double kGlassMorphSpacing (16) How far the neck reaches while the glass moves. Zero: no neck and no group.
finish GlassFinish? null The glass. Null takes the theme's.
labelled bool true Whether a label is drawn over the glass, so the theme's label floor applies.
onEnd VoidCallback? null Called when a morph has settled.