g1455

Misuse & common errors

What goes wrong with glass and why: missing glass, holes, grey boxes, a capture on every frame, and the debug messages the package prints, each with its fix.

Most mistakes with glass fail quietly: no exception, only glass that is missing, grey, or captured on every frame. Each entry below is a symptom, its cause and the fix. Where a debug build says something, the message is quoted as it starts.

To see where the glass is, set debugPaintGlassSurfaces = true in a debug build: every surface gets a cyan outline.

Setup

Only the children are drawn, with no glass

A surface samples the capture of the GlassHost above it. With no host above, it paints its child over nothing, and says nothing. Put one host in the app's builder:, as in Installation.

"GlassAlert was built with no GlassHost above it."

The same error names "A glass sheet", or the panel of a menu or a popover. A dialog and a sheet are built in the navigator's overlay, and a menu and a popover in the nearest overlay, so a host around one screen, inside the route, does not reach them. That includes the host GlassScaffold mounts when there is none above it. The check runs in debug only; in release the modal shows without glass. Move the host above the navigator:

MaterialApp(
  builder: (BuildContext context, Widget? child) => GlassHost(child: child!),
  home: const HomePage(),
)

A GlassTheme above the host changes nothing

The host installs a theme of its own, built from its parameters, for everything under it, so a GlassTheme above the host is overridden. Declare screen-wide settings on GlassHost, and put a GlassTheme below it to change a subtree.

The first frames show no glass, then a blur with no tint or rim

Two delays, of which only one is avoidable. The host captures a frame after it has been painted, so the first frame of any screen has no glass; that is by design. The shaders compile asynchronously, and until they arrive, glass that has a capture draws the blurred backdrop clipped to its shape, without tint, rim or bend. await GlassHost.precache() in main() compiles them before the first frame. A shader that fails to load is reported "while loading a glass shader", and the glass keeps drawing that stand-in.

What the glass shows

A hole over a video, a map or a platform view

The host captures what is under its glass by painting that part of the tree a second time. A platform view, a Texture, a video or a camera preview paints outside Flutter's pictures and records nothing. Wrap it in GlassProxy.replace with a stand-in, as on Capture control. A stand-in changes what the glass sees, not when the host captures: a video that composites a new frame is still a change.

Glass inside an Opacity or a fade disappears

An Opacity below 1, an AnimatedOpacity or a FadeTransition mid-animation, a ColorFilter or an ImageFiltered opens a layer, and glass inside that layer does not survive it. On the cheap tier the rim and the press highlight, which add light to what is under them, add it to the layer instead: one Opacity(0.99) above a button takes its press from +50 code values to +4.

To fade glass, animate the surface's materialize (blur first, tint last); to hide it, use Visibility. The package does both itself: its alert materializes, and a menu hides its anchor button with Visibility.

// Not Opacity(opacity: t, child: GlassSurface(...)).
GlassSurface(materialize: t, child: label)

Content that fades under the glass is fine: the host sees the fade and captures it.

A bar over glass cards shows the page with the cards cut out

The bar is the cards' sibling, and sibling glass does not see sibling glass. Wrap the bar in GlassAbove, which raises it a level so it refracts the cards. Glass nested inside glass, a scroll edge and the package's modals are lifted already.

An unlifted bar shows up blurred inside a scroll edge

Levels are a declaration, not paint order: glass beside a lifted subtree is drawn into its capture even when it is painted on top of it. Lift the bar with the edge; GlassScrollEdge.child does that for you.

A surface that replaces another at the same place draws nothing

A known issue, not yet fixed: when a host replaces one surface with another at exactly the same rect, it keeps the old capture, and the new surface draws nothing until something else under the glass changes.

A tab bar with minimizeBehavior: onScrollDown never collapses

A scroll notification travels only up the tree, and the bar is the scroll view's sibling, not its child, so it hears nothing by itself. Put a GlassTabBarMinimizer above both the scroll view and the bar; a GlassScaffold is one already. Only the nearest vertical scroll view counts: a list nested in another scroll view, or a horizontal one, does not collapse the bar. See Tab bar.

Look and legibility

"A glass component has no GlassThemeData.backdrop, and its finish is not legible over every backdrop"

Printed once, in debug. Nothing said what is behind the glass, so the label was chosen against every backdrop, and none reaches WCAG AA over the worst of them. Declare the screen's background as GlassHost(backdrop: ...) if it is flat, or richBackdrop: true with minLabelContrast: kTextContrastAA over an image or a feed: the glass is then dimmed until the label reaches the floor. Or let the glass measure it: adaptive glass. See What the app declares.

"GlassTier.opaque with no GlassThemeData.backdrop declared."

The opaque tier transmits nothing, so it fills with the level the glass would show over the declared backdrop. Without one the fill is the finish's tint itself, much darker than the glass: 29 of 255 for GlassFinish.regularDark, against the 69 the glass shows over mid-grey. Declare backdrop: whenever the opaque tier can be chosen, which includes every app that passes Reduce Transparency to a GlassTierPolicy.

Glass over a flat colour is a grey box

Glass shows what is behind it, bent, blurred and tinted; over one flat colour that is the same colour, tinted. Put glass over content, in a Stack. Over a plain page, a plain Container or Card is the honest choice.

A lens or a held drop looks grey

A surface counts as labelled by default, and the label floor (minLabelContrast) dims the glass for a label it does not have. Pass labelled: false to glass that carries no text.

Labels or icons in a bar have the wrong colour

GlassBar, GlassCard, GlassButton and the modals set DefaultTextStyle and IconTheme to the legible colour, black or white. A colour hard-coded inside them overrides it, and so does a widget that takes its colour from the app's ThemeData rather than from those two. Leave the colour out, or pass IconTheme.of(context).color on.

Adaptive glass does not adapt

GlassHost(adaptive: GlassAdaptive()) re-picks the glass for bars, cards and buttons only. A raw GlassSurface, a GlassScrollEdge, the tab bar and the segmented control keep the branch picked from backdrop, and a fused group's members adapt only their labels. A finish that is named anywhere, on the surface, the component, the host or a theme, is held, and only its label follows the reading.

"N of M surfaces in a GlassGroup name their own finish."

A fused group is one draw with one set of optics, so the group's finish is what ships and the members' are ignored. Put the finish on the GlassGroup, or take the surface out of it. For the same reason a member of a fusing group draws no fade and no ripple.

"A GlassGroup holds 13 surfaces; the fused draw carries 12."

kMaxFusedShapes is 12. Past it the group is refused rather than truncated: its members draw themselves, and the bridges between them are missing. Split the group, or fuse fewer surfaces.

A ripple does not show

GlassRipple is off while the platform asks for reduced motion, below GlassTier.full, and on a member of a fusing group. It is also off by default: declare it on GlassHost.ripple or on the surface.

A panel appearing through presence narrows to a line

presence erodes the shape and is for budding inside a GlassGroup; a lone panel erodes to its middle line. Animate materialize to make a panel appear or leave.

A shape inside glass sits badly in its corners

A highlight, a thumb or an inner panel inset in glass looks off when its corner does not share the glass's centre of curvature. Its radius is the glass's less the inset: GlassConcentric.radius(outer, inset), or GlassConcentric.borderRadius(outer, insets) per corner, rather than a number that agrees by accident until one of the two moves. See GlassSurface.

A badge drawn as glass is a different red over every backdrop

Apple's badge is opaque, on the glass and not of it, so that it reads at a glance over anything. A translucent glass pill is one more surface and a colour that changes with what is behind it. Use GlassBadge, a plain capsule that costs no surface.

Cost

Moving glass captures on every frame

A surface's slot in the capture is its own box, so glass that moves is captured again wherever it goes. Wrap the region it moves in in a GlassTravel: the host captures the whole region once, and moving over still content inside it captures nothing. The switch, slider, segmented control and tab bar already do this.

Glass inside a GlassTravel still captures as it moves

Motion is free only if moving the glass repaints nothing else, because a repaint under the glass is changed content. Put the still content behind a RepaintBoundary of its own and the moving glass behind another, so the moving glass's parent paints nothing. A surface that leaves the region is captured again.

A minus and a plus are two surfaces

Two GlassButtons side by side are two glasses. A GlassStepper is one capsule, with the divider and the held half drawn inside it, and a press that costs no capture.

The search bar captures when it takes or loses the focus

Cancel slides in as the field takes the focus, and the field's glass narrows to make room: glass whose box changes is retaken, a capture a frame for cancelDuration, 16 over 250 ms at 60 Hz in the package's tests, each way. A GlassTravel does not help, because it is the resize that costs, not Cancel's paint. showsCancelButton: false keeps the box still; under reduced motion the slide is one frame. See Search bar.

A change inside a large sheet captures

An opaque large sheet (the default largeFinish) reads no backdrop and is drawn on the cheap tier, which saves its capture: one surface fewer in it, 20,608 px² against 337,408 headless. But a cheap surface is ordinary content to the capture of the glass around it, so a change inside the sheet is a retake where a glass sheet took none. A sheet whose content animates may be cheaper with a translucent largeFinish. See Sheet.

Every press of a button captures twice

The press swells the glass inside a travel region declared from touch-down until the spring settles: one capture as the region appears and one as it goes, nothing while it moves and nothing at rest. GlassPress.none, on GlassHost.press, a theme or one button, keeps the box still and builds no region.

A tab bar rebuilt by a bare setState captures once

A known cost, not yet traced: a setState that rebuilds a GlassTabBar costs one capture even when nothing it draws changed. Rebuild the bar when its selection or its items change, not with every change of the screen around it.

A page control captures while the pages turn

It does not: the PageView moving under the glass does, 43 captures a swipe in the package's tests, the same with the dots following and with dots that stay put. Content that moves under glass is a capture a frame. See Page control.

A slider rounded in onChanged is told every frame

Rounding the value yourself leaves the knob between the stops and calls onChanged on every frame of a drag. Pass divisions: every input lands on a stop, and onChanged is called only when the stop changes. See Slider.

A custom finish costs more than the preset it came from

A finish's name is its key into the measured quality tables. A name that is not in them gets no measured damage, so the host does not lower the capture's resolution for it. Derive a custom finish with copyWith from the preset it is closest to, which keeps the name.

Too much glass

Each surface is one more draw, and the cost grows faster than the count. Keep glass to the navigation and controls layer, not every card of a feed. A row of icon actions is one GlassButtonGroup, and plain icons inside a bar are cheaper than glass buttons, which are glass on glass: a level, and a snapshot per captured frame. GlassGroup and GlassUnion are a look, not a saving: twelve clustered surfaces grouped cost 1.60 to 1.65 times the same twelve ungrouped. See Performance.

Glass is coarser on a large window

The capture must fit the GPU's texture limit, and the host's only answer to hitting it is a coarser capture. The limit it assumes is the specification's floor, not the device's: 4096 on Vulkan, 8192 on Metal, 2048 on GLES. Every GPU the package was measured on allocates 16384; a host that knows its device can say so with GlassHost(maxTextureSide:).

Every frame captures, still screen or not

GlassHost(content: GlassContentDeclaration.undeclared) re-captures on every frame. It exists to rule the change detection out, and is the most expensive thing the package can be asked to do; don't ship it. The same goes for maxCaptures, resolution, blurPass and package:g1455/glass_diagnostics.dart.

Slow on the web outside Chromium

CanvasKit reads every capture back from the GPU. Build with --wasm, and where the app still runs on CanvasKit, declare the cheap tier. See Platforms & web.

Platform settings

Reduce Transparency, increased contrast or thermal state change nothing

Flutter does not pass Reduce Transparency or thermal state on, nor increased contrast on macOS, and the package ships no platform code to read them. Read them natively and declare them: GlassTierPolicy(reduceTransparency: ...), GlassHost.highContrast, GlassHost.thermal. Tiers never change on their own. See What the app declares.

The user's Reduce Transparency is ignored

GlassTierPolicy(pinned: ...) overrides everything, the user's setting included. Pin a tier in tests and benchmarks, never for users; for a low-end device, pass a ceiling instead.

Widget tests

No glass on the first pump

Glass widgets need a GlassHost above them in the test as in the app, and above the Navigator for modals. The first frame has no glass, because the capture lands a frame later: pump at least one more frame before looking for it.

await GlassHost.precache() never completes

The shader load needs real time, and a widget test's fake clock does not give it. Alternate tester.runAsync with tester.pump until the future completes, or leave the precache out: the host loads the shaders on its own.