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.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MixScope(
colors: {
$primary: Colors.blue,
$secondary: Colors.green,
$background: Colors.white,
},
spaces: {
$spacingSm: 8.0,
$spacingMd: 16.0,
$spacingLg: 24.0,
},
radii: {
$radiusSm: Radius.circular(4),
$radiusMd: Radius.circular(8),
},
textStyles: {
$headingStyle: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
$bodyStyle: TextStyle(fontSize: 16),
},
child: MaterialApp(
home: MyHomePage(),
),
);
}
}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 three ways to reference a token. They all 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())
.paddingAll($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 of values and swap them in MixScope.
// Define tokens
final $background = ColorToken('background');
final $foreground = ColorToken('foreground');
final $surface = ColorToken('surface');
// Light theme values
final lightTheme = {
$background: Colors.white,
$foreground: Colors.black,
$surface: Colors.grey[100]!,
};
// Dark theme values
final darkTheme = {
$background: Colors.grey[900]!,
$foreground: Colors.white,
$surface: Colors.grey[800]!,
};
class ThemedApp extends StatefulWidget {
@override
State<ThemedApp> createState() => _ThemedAppState();
}
class _ThemedAppState extends State<ThemedApp> {
bool isDark = false;
@override
Widget build(BuildContext context) {
return MixScope(
colors: isDark ? darkTheme : lightTheme,
child: MaterialApp(
home: Scaffold(
body: Box(
style: BoxStyler().color($background()),
child: Column(
children: [
StyledText(
'Hello, World!',
style: TextStyler().color($foreground()),
),
ElevatedButton(
onPressed: () => setState(() => isDark = !isDark),
child: Text('Toggle Theme'),
),
],
),
),
),
),
);
}
}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: EdgeInsets.all(spacing),
child: Text('Resolved tokens'),
);
}Creating Custom Tokens
The built-in token types cover most styling needs. If you need a token for a type they don’t cover (e.g. EdgeInsetsGeometry), you can create one by extending MixToken<T> and providing a reference class.
/// Token for EdgeInsetsGeometry values
class EdgeInsetsGeometryToken extends MixToken<EdgeInsetsGeometry> {
const EdgeInsetsGeometryToken(super.name);
@override
EdgeInsetsGeometryRef call() => EdgeInsetsGeometryRef(Prop.token(this));
}
/// Reference class — wraps the Prop so it can be used in styles
class EdgeInsetsGeometryRef extends Prop<EdgeInsetsGeometry> {
EdgeInsetsGeometryRef(Prop<EdgeInsetsGeometry> prop) : super.fromProp(prop);
}Custom tokens use the generic tokens map in MixScope (instead of colors, spaces, etc.):
final $contentPadding = EdgeInsetsGeometryToken('padding.content');
final $cardPadding = EdgeInsetsGeometryToken('padding.card');
MixScope(
tokens: {
$contentPadding: EdgeInsets.all(16),
$cardPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
},
child: MyApp(),
);
// Use via merge + .create() since custom tokens work through Prop
final style = BoxStyler()
.color(Colors.red)
.size(100, 100)
.merge(.create(padding: $contentPadding()));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. To get the real value outside of Mix, 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))