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.
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:
Built-in Token Types
Mix provides token types for common styling needs:
| Token Type | Value Type | Use Case |
|---|---|---|
ColorToken | Color | Colors and backgrounds |
SpaceToken | double | Spacing values (padding, margin) |
DoubleToken | double | Any numeric value |
RadiusToken | Radius | Border radii |
TextStyleToken | TextStyle | Typography styles |
BorderSideToken | BorderSide | Border definitions |
ShadowToken | List<Shadow> | Text shadows |
BoxShadowToken | List<BoxShadow> | Box shadows |
FontWeightToken | FontWeight | Font weights |
DurationToken | Duration | Animation durations |
BreakpointToken | Breakpoint | Responsive breakpoints |
Using Tokens in Styles
Mix offers two ways to reference a token. Both resolve to the same value — the difference is ergonomics.
Call syntax (recommended)
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.
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())
.borderRounded(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, or any other context-derived value.
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 winsOverriding 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 source | MixScope maps | Resolver reading BuildContext |
Requires MixScope? | Yes | No (scope override is optional) |
| Best for | Design-system values you swap by theme | Bridging Material’s theme (ColorScheme, TextTheme) and other BuildContext-derived values into Mix |
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
- Directives —
Prop.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