Legibility & theme
How glass components choose black or white labels, keep them readable, and how a subtree gets its own look.
GlassTheme · GlassThemeData · GlassLegibility · kTextContrastAA · Source
Text on glass sits over whatever the glass sits over, so its colour cannot be fixed in advance. The components (GlassBar, GlassButton, GlassCard, the text field, the alert, the menu and the popover) choose black or white for their labels, from the finish and from what you told the host is behind the glass.
All of that lives in a GlassTheme: the host installs one, and you can nest another to give a subtree its own finish,
tier or backdrop.
When to use
- Declare the backdrop on the host, always:
backdrop:for a flat colour,richBackdrop: truefor images and feeds. - Add
minLabelContrast: kTextContrastAAwhen text sits on clear glass or over bright content. - Nest a
GlassThemebelow the host when one part of a screen differs: a light panel, a cheaper list. - Read
GlassTheme.of(context).legibility()when you put your own text on a rawGlassSurface. - Don't put a
GlassThemeabove the host: the host installs its own and overrides it.
Usage
// A panel over a light page, below the app's host.
GlassTheme(
data: GlassTheme.of(context).copyWith(
backdrop: const Color(0xFFF2F2F7),
finish: GlassFinish.regularLight,
),
child: const GlassCard(child: Text('Black text, chosen for you')),
)
How the label is chosen
- A flat backdrop (
backdrop:declared,richBackdropfalse): the label is whichever of black or white stands out more against the glass laid over that colour. Exact, because every pixel under the glass is that colour. - A rich backdrop, or none declared: the label is whichever has the better worst case over any backdrop. In debug, the package warns once when that worst case cannot reach WCAG AA.
- A contrast floor (
minLabelContrast:): when neither colour reaches it, the glass is dimmed by the least amount that does. Dimming is a change of tint, so it costs nothing to draw.regularDarkneeds no dim for AA over any backdrop;cleardoes.
Glass with labelled: false has no label to protect and is never dimmed.
The demo shows it: drag the backdrop from dark to light and watch the label flip and the contrast change, then switch the floor on with clear glass.
Custom glass
A raw GlassSurface does not colour its child. Ask the theme:
final GlassLegibility look = GlassTheme.of(context).legibility(GlassFinish.clear);
Text('Now playing', style: TextStyle(color: look.label));
look.finish is the finish actually drawn (dimmed, if a floor asked for it), and look.rim the opaque outline under
increased contrast, or null.
Note
GlassThemeData.copyWith cannot set a nullable field back to null. To drop minLabelContrast or backdrop for a
subtree, build a new GlassThemeData.
Code
import 'package:flutter/material.dart';
import 'package:g1455/g1455.dart';
/// A light settings panel inside a dark app, and custom glass that reads
/// its label colour from the theme. Assumes a GlassHost above.
class LightPanel extends StatelessWidget {
const LightPanel({super.key});
static const Color page = Color(0xFFF2F2F7);
@override
Widget build(BuildContext context) {
final GlassThemeData outer = GlassTheme.of(context);
return ColoredBox(
color: page,
child: GlassTheme(
// Inherit the host's tier and contrast; only the backdrop and finish differ.
data: outer.copyWith(
backdrop: page,
richBackdrop: false,
finish: GlassFinish.regularLight,
minLabelContrast: kTextContrastAA,
),
child: Builder(
builder: (BuildContext context) {
final GlassLegibility look = GlassTheme.of(context).legibility(GlassFinish.clear);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// A component picks its own label colour.
const GlassCard(child: Text('Notifications')),
const SizedBox(height: 16),
// A raw surface: take the colour from the theme.
SizedBox(
width: 200,
height: 56,
child: GlassSurface(
borderRadius: kGlassCapsule,
finish: GlassFinish.clear,
child: Center(
child: Text('Clear glass', style: TextStyle(color: look.label)),
),
),
),
],
);
},
),
),
);
}
}API
| Parameter | Type | Default | Description |
|---|---|---|---|
finish |
GlassFinish |
GlassFinish.regularDark |
The material every surface below wears unless it names its own. |
tier |
GlassTierChoice |
GlassTierChoice.byDefault |
Which rung is drawn: full, cheap or opaque. |
backdrop |
Color? |
null |
The average colour behind the glass. Chooses the label over a flat page; fills the opaque tier. |
highContrast |
bool |
false |
Draws an opaque outline instead of the calibrated rim. |
richBackdrop |
bool |
false |
The backdrop is an image or feed: labels are chosen for the worst case. |
minLabelContrast |
double? |
null |
The least label contrast. The glass is dimmed just enough to meet it. |
ripple |
GlassRipple? |
null |
The default touch wave. |
GlassTheme
| Member | Description |
|---|---|
GlassTheme({required GlassThemeData data, required Widget child}) |
Gives a subtree its own glass configuration. Must be below the host. |
GlassTheme.of(context) |
The theme in force, or the defaults with the platform's .regular branch. |
GlassTheme.maybeOf(context) |
The theme in force, or null. |
GlassThemeData.legibility([GlassFinish? own, bool labelled = true]) |
The finish to draw, the label colour and the outline, for this theme. |
GlassLegibility
| Field | Type | Description |
|---|---|---|
finish |
GlassFinish |
The finish to draw: the declared one, or it dimmed to meet the floor. |
label |
Color |
Black or white, whichever reads best. |
rim |
Color? |
The opaque outline under increased contrast, or null. |
Constants
| Name | Value | Description |
|---|---|---|
kTextContrastAA |
4.5 |
WCAG AA for body text. |
kNonTextContrast |
3 |
WCAG's floor for non-text elements. |