Skip to content

Every flow_ui component draws from two token sets — colors and typography — installed as a ThemeExtension:

main.dart
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.

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.

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.

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.)

Start from a preset and replace what your brand needs:

Override the accent
MaterialApp(
theme: ThemeData(
extensions: [
FlowTheme(
colors: FlowColors.light.copyWith(
primary: const Color(0xFF6750A4),
),
),
],
),
)

Your own widgets can read the same tokens the components use:

Read tokens in your own widgets
final colors = context.flowColors;
final typography = context.flowTypography;
Text(
'Caption',
style: typography.bodySmall.copyWith(color: colors.onSurfaceVariant),
)

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:

Your brand's typeface
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.

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:

Restyle every instance once
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.

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.