Theming
The flow_ui token system: three levels of ink, translucent washes on opaque grounds, and a color scheme shaped like Material 3.
Every flow_ui component draws from two token sets — colors and
typography — installed as a ThemeExtension:
MaterialApp( theme: ThemeData(extensions: [FlowTheme.light()]), darkTheme: ThemeData( brightness: Brightness.dark, extensions: [FlowTheme.dark()], ),)No component hardcodes a color or a text style, so restyling the whole library is a matter of overriding tokens.
Three levels of ink
Section titled “Three levels of ink”Color role names follow Material 3’s ColorScheme, so an existing M3 scheme
maps straight across — with three additions. The Flow design draws content at
three strengths, and M3 only names two:
| Token | Strength | Used for |
|---|---|---|
onSurface |
100% | Prose, the model name, an active label |
onSurfaceVariant |
75% | Secondary content: row labels, icons at rest |
onSurfaceMuted |
50% | Muted chrome: placeholders, carets, action icons |
Below the ramp sits onSurfaceDisabled (30%) — not a fourth content level
but the drained state: the send disc with nothing to send.
The other addition is a pair of status groups beside error: success and
warning, each with its on, Container and onContainer roles.
The third is shadow — the ink at 2% with the alpha baked in. Every raised
element — the composer, the menu card, attachment tiles, the jump-to-latest
disc — draws its shadow in this one color with its own blur and no offset,
so retheming it lifts or kills the library’s shadows at once.
Three rules hold the palette together
Section titled “Three rules hold the palette together”The ink, the outlines, and the container ladder are translucent.
onSurfaceVariant, onSurfaceMuted, outline (the faint hairline — 7%
ink in light, 8% in dark), outlineVariant (the firm one — 12% in both)
and shadow (2%) are the foreground ink at an alpha, not resolved colors —
the same label and the same hairline read correctly on the page and on a
raised card, because they composite. The surfaceContainerLowest → Highest
ladder is cut from the same ink in even steps — 2, 4, 6, 8, 10% — so a
fill picked for the page holds up anywhere.
Accent containers are washes of their accent. Each Container is its
accent at an alpha — 8% for primary, secondary and tertiary, 6% for error,
success and warning — and each onContainer is the accent itself.
The grounds are opaque. surface — warm paper in light, #171717 in
dark — and surfaceBright are what everything else composites onto, so
they are flat colors.
The raised card
Section titled “The raised card”surfaceBright is the composer, the menus, the sheets — the one surface
that lifts off the page in both themes. That’s why it sits outside the
surfaceContainerLowest → Highest tint ladder rather than at the bottom of
it: in light it’s pure white above warm paper, in dark it’s the lifted
#1E1E1E above #171717. (surfaceContainerLowest is the ladder’s
faintest rung — a 2% wash — not the card; retheme the card through
surfaceBright.)
Overriding tokens
Section titled “Overriding tokens”Start from a preset and replace what your brand needs:
MaterialApp( theme: ThemeData( extensions: [ FlowTheme( colors: FlowColors.light.copyWith( primary: const Color(0xFF6750A4), ), ), ], ),)Your own widgets can read the same tokens the components use:
final colors = context.flowColors;final typography = context.flowTypography;
Text( 'Caption', style: typography.bodySmall.copyWith(color: colors.onSurfaceVariant),)The type scale
Section titled “The type scale”FlowTypography.standard follows the Material 3 scale — display, headline,
title, body and label, each in large / medium / small — set in Google Sans.
Letter-spacing stays at zero. Line heights step with the role: 1.5 for
body, 1.3 for title and label, 1.2 for headline, 1.15 for display.
Each body size carries three weights of ink — bodyLarge,
bodyLargeEmphasised (w500), bodyLargeDark (w600), and likewise for
Medium and Small. Title and label pair the regular cut with an emphasised
one (titleLargeEmphasised at w600, labelLargeEmphasised at w500), so
emphasis in a heading or a control never needs an ad-hoc copyWith.
Styles are colorless; components pair them with color tokens at draw time.
To keep the scale and swap the face:
FlowTheme( colors: FlowColors.light, typography: FlowTypography.standard.withFontFamily('Inter'),)withFontFamily rebuilds every style from the four things the scale
carries — size, weight, line height, tracking — and takes a package: for
a font that ships inside one.
Component styles
Section titled “Component styles”Between the tokens and a single widget sit the component styles —
Material’s component-theme tier. Each major widget has a FlowXStyle
data bag of optional overrides (FlowMenuStyle, FlowMarkdownStyle,
FlowComposerStyle, FlowMessageStyle, FlowCodeBlockStyle,
FlowConfirmationStyle, FlowErrorStateStyle,
FlowMessageActionsStyle, FlowPillStyle,
FlowSuggestionStyle, FlowToastStyle), and the theme can carry an app-wide default for
each:
FlowTheme( colors: FlowColors.light, markdownStyle: const FlowMarkdownStyle(linkColor: brandBlue), codeBlockStyle: const FlowCodeBlockStyle(borderColor: Colors.transparent),)Resolution is field by field: a widget’s own style object wins over the
theme’s, and any field neither sets falls through to the tokens. Text
style fields merge over their role’s base, so setting one property never
restates the rest. Every field is documented on its style class, with
the token it defaults to. The Restyling section on each component’s
page lists its fields.
Spacing and radii are not tokens
Section titled “Spacing and radii are not tokens”Following Material’s structure, each component bakes its own metrics from
the design file and exposes per-widget overrides (padding:,
borderRadius:) where hosts retheme. There is deliberately no global
spacing scale to fight with.