Skip to Content
DocsMixGuidesDesign Tokens

Design Tokens

When you hardcode Colors.blue or EdgeInsets.all(16) across your app, changing your brand color or spacing scale means hunting through dozens of files. Design tokens solve this: you define a named value once, provide it at the top of your widget tree, and every style that references it updates automatically — including when you switch themes.

Mix provides built-in support for tokens through MixToken and MixScope.

A token’s value can come from one of two places:

  • A MixScope — you provide concrete values near the top of the tree and swap them to switch themes. This is the approach most of this guide covers.
  • The build context — a ContextToken derives its value directly from BuildContext, with no MixScope required. This is the idiomatic way to bridge Material’s theme (Theme.of(context).colorScheme, textTheme) into Mix, so your styles track the active ThemeData automatically. Jump to Context Tokens if that’s what you’re here for.

Getting Started

Using a token takes three steps: declare it, provide a value, and reference it in a style.

// 1. Declare the token (the $ prefix is a naming convention to distinguish tokens from regular variables) final $primary = ColorToken('primary'); // 2. Provide the token value via MixScope MixScope( colors: { $primary: Colors.lightBlue, }, child: MyApp(), ); // 3. Reference the token in a style — call() resolves it from MixScope at build time final style = BoxStyler() .color($primary()) .size(100, 100);

MixScope

MixScope is the widget that provides token values to all its descendants. It works like Flutter’s Theme and ThemeData pattern — place it near the top of your tree and every descendant can resolve tokens from it.

MixScope( colors: { $primary: Colors.blue, $background: Colors.white, }, spaces: { $spacingMd: 16.0, }, radii: { $radiusMd: Radius.circular(8), }, child: MaterialApp(home: MyHomePage()), );

Nested MixScope widgets override token values for their subtree. The preview below shows an outer scope providing blue, and an inner scope overriding the same token to red for a child widget:

Resolving preview metadata...

Built-in Token Types

Mix provides token types for common styling needs:

Token TypeValue TypeUse Case
ColorTokenColorColors and backgrounds
SpaceTokendoubleSpacing values (padding, margin)
DoubleTokendoubleAny numeric value
RadiusTokenRadiusBorder radii
TextStyleTokenTextStyleTypography styles
BorderSideTokenBorderSideBorder definitions
ShadowTokenList<Shadow>Text shadows
BoxShadowTokenList<BoxShadow>Box shadows
FontWeightTokenFontWeightFont weights
DurationTokenDurationAnimation durations
BreakpointTokenBreakpointResponsive breakpoints

Using Tokens in Styles

Mix offers two ways to reference a token. Both resolve to the same value — the difference is ergonomics.

The simplest approach. Call the token like a function to get a reference that resolves at build time:

final style = BoxStyler() .color($primary()) .padding(.all($spacingMd()));

Prop.token (for directives)

Use Prop.token() with .create() when you need to chain directives on a token value — for example, doubling a spacing token:

final style = BoxStyler.create( width: Prop.token($baseSize).multiply(2), padding: Prop.token($spacingMd), );

For most styles, use the call syntax. Reach for Prop.token() only when you need directives like .multiply(), .add(), or .clamp().

Theme Switching

Tokens make theme switching straightforward: define two maps and swap them in MixScope.

final $background = ColorToken('background'); final $foreground = ColorToken('foreground'); final lightColors = { $background: Colors.white, $foreground: Colors.black, }; final darkColors = { $background: Colors.grey[900]!, $foreground: Colors.white, }; // Swap themes by toggling the map MixScope( colors: isDark ? darkColors : lightColors, child: MyApp(), );

Every style referencing $background or $foreground updates automatically when the map changes.

Resolving preview metadata...

Context Tokens

The most common reason to reach for a context token is bridging Material’s theme into Mix. Your app already has a ThemeData — its ColorScheme, TextTheme, and other values live on Theme.of(context). Re-declaring those as Mix tokens and re-providing them in a MixScope duplicates the source of truth: every Material 3 color, every brightness switch, every ThemeData override would have to be mirrored by hand.

ContextToken<T> connects the two systems directly. You give it a resolver that reads from BuildContext, and it computes its value at resolution time straight from the ambient Material theme — no MixScope required.

// Map Material's ColorScheme into Mix tokens — declared once, app-wide. final $primary = ContextToken<Color>((context) => Theme.of(context).colorScheme.primary); final $surface = ContextToken<Color>((context) => Theme.of(context).colorScheme.surface); final $onSurface = ContextToken<Color>((context) => Theme.of(context).colorScheme.onSurface); // Use them like any other token — they track the active ThemeData automatically. final cardStyle = BoxStyler() .color($surface()) .borderRadius(.circular(12)); Box(style: cardStyle);

Because the resolver runs against the consuming widget’s context, these tokens stay in sync with Material for free: toggle MaterialApp’s themeMode, change colorScheme, or sit under a local Theme override, and every Mix style referencing $primary follows — without touching a MixScope. The same pattern works for Theme.of(context).textTheme, MediaQuery, and other context-derived values as long as T is one of Mix’s built-in token value types (e.g. Color, double, Radius, TextStyle). For unsupported types, call .resolve(context) directly or use a custom tokenContextToken<T>.call() only produces token refs for Mix’s supported value set.

Because the value flows through the same token-reference pipeline as every other token, it keeps exact fluent-chain ordering:

BoxStyler().color($primary()).color(Colors.red); // red wins BoxStyler().color(Colors.red).color($primary()); // primary wins

Overriding a context token

A matching entry in MixScope still wins, so the resolver acts as a context-derived default that any scope can override:

MixScope( tokens: {$primary: Colors.green}, // overrides the resolver for this subtree child: Box(style: BoxStyler().color($primary())), );

ContextToken equality is based on the resolver’s identity, not a name. Two tokens built from the same top-level or static function are equal; two built from look-alike inline closures are not. Declare context tokens as top-level or static finals so overrides and merges match the token you expect.

Context tokens vs. scope tokens

Scope token (ColorToken, …)ContextToken
Value sourceMixScope mapsResolver reading BuildContext
Requires MixScope?YesNo (scope override is optional)
Best forDesign-system values you swap by themeBridging Material’s theme and other context-derived values whose type is in Mix’s built-in token set

Resolving Tokens Programmatically

You can resolve tokens outside of styles using BuildContext. This is useful when you need a token value in non-Mix code, such as a plain Flutter widget:

@override Widget build(BuildContext context) { final primaryColor = $primary.resolve(context); final spacing = $spacingMd.resolve(context); return Container( color: primaryColor, padding: .all(spacing), child: Text('Resolved tokens'), ); }

Best Practices

  • Use the $ prefix for token variables ($primary, $spacingMd) — it makes tokens visually distinct from regular values at a glance
  • Use hierarchical names for the token string identifier: ColorToken('color.primary'), SpaceToken('spacing.md')
  • Group related tokens in separate files (tokens/colors.dart, tokens/spacing.dart) to keep declarations organized
  • Prefer tokens over hardcoded values — even if you only have one theme today, tokens make future changes a single-line edit instead of a find-and-replace

Common Pitfalls

Using $token() outside of Mix APIs. Calling a token (e.g. $primary()) returns a placeholder value, not the real resolved value. Mix replaces this placeholder internally when the style is resolved. If you pass $primary() to a plain Flutter widget like Container(color: $primary()), you’ll get the placeholder — not your actual color. Use $token.resolve(context) instead:

// Wrong — $primary() is a placeholder, not a real Color Container(color: $primary()) // Correct — resolve gives you the actual value from MixScope Container(color: $primary.resolve(context))

See Also

  • Styling — using tokens inside styler chains
  • DirectivesProp.token() with .multiply(), .add(), and other transforms
  • Creating Custom Tokens — building your own token types for values not covered by built-ins
  • Theming Tutorial — step-by-step guide to building a full themed app
Last updated on