Design Tokens: as a design system in Flutter projects
01 INTRODUCTION
Overview
In the world of digital product development, design consistency is fundamental. As products grow, expand across multiple platforms (web, iOS, Android) and are developed by multiple teams, maintaining a coherent UI and UX becomes a complex challenge. Without a robust system, design values (such as colors, fonts, spacing, and border radii) tend to be manually hard-coded in different ways for each platform, leading to visual discrepancies, inefficiencies, and increased technical debt.
Purpose
The problem that Design Tokens aim to solve is: providing a single source of truth for all design decisions.
Concept — W3C Definition
Design Tokens are all the design decisions that define the aesthetic properties of a design system — from colors to font sizes, from spacing to radius values. They are the modern evolution of hard-coded values, acting as indivisible pieces of a design system.
EXAMPLE:
Design tokens Definition: “$color-brand-primary”: “#FF0000”
Hard-coded mode : #FF0000Technical Info
Design tokens are stored in a central, platform-agnostic repository, establishing a single source of truth for the entire suite of digital products. This centralized management allows teams to use exactly the same design values across all platforms (web, mobile apps, documentation, etc.). Updating a token — for example, changing the value of $color-brand-primary — instantly propagates that change consistently throughout the system, ensuring brand consistency and drastically reducing maintenance time and the risk of visual regressions.
EXAMPLE — Flow Overview
Figma / Design Tool
│
│ (Tokens Studio plugin or native export)
▼
tokens.json ←── Single Source of Truth (W3C format)
│
│ (Style Dictionary / figma2flutter / custom script)
▼
Dart generated files
├── app_colors.dart
├── app_typography.dart
├── app_spacing.dart
├── app_radius.dart
└── app_theme.dart
│
▼
Flutter App (ThemeData + ThemeExtensions)02 FIGMA AND DESIGN TOKENS
Overview
Figma is a popular tool for designing user interfaces and managing design systems. It is the ideal place where designers can define and organize Design Tokens. Through native features or plugins, it is possible to create variables for colors, typography, spacing, shadows, and other design attributes.
EXAMPLE —Design Definitions
- A color
$color-brand-primarywith a certain hexadecimal value - A font size
$font-size-heading-largewith a value inpx - A spacing value
$spacing-mediumwith a value inpx
These tokens are then used directly in design components, ensuring that all visual elements use the values defined in the system.
03 EXPORTING DESIGN TOKENS TO JSON
Overview
Once defined in Figma (or any other design tool), Design Tokens must be exported in a format that is readable and usable by developers. The JSON format is the standard choice for this purpose, as it is platform-agnostic and easily parseable by any programming language.
The W3C specification for Design Tokens emphasizes the importance of a standardized export format. This JSON format should represent the hierarchy and types of the tokens (e.g., color, dimension, fontFamily).
EXAMPLE:
{
"$schema": "https://www.designtokens.org/schemas/2025.10/format.json",
"color": {
"brand": {
"primary": { "$value": "#007AFF", "$type": "color" },
"primary-hover": { "$value": "#005EC4", "$type": "color" },
"secondary": { "$value": "#5AC8FA", "$type": "color" }
},
"neutral": {
"100": { "$value": "#F5F5F5", "$type": "color" },
"500": { "$value": "#9E9E9E", "$type": "color" },
"900": { "$value": "#212121", "$type": "color" }
},
"semantic": {
"error": { "$value": "{color.system.red-500}", "$type": "color" },
"success": { "$value": "{color.system.green-500}", "$type": "color" },
"warning": { "$value": "{color.system.amber-400}", "$type": "color" }
}
},
"spacing": {
"xs": { "$value": { "value": 4, "unit": "px" }, "$type": "dimension" },
"sm": { "$value": { "value": 8, "unit": "px" }, "$type": "dimension" },
"md": { "$value": { "value": 16, "unit": "px" }, "$type": "dimension" },
"lg": { "$value": { "value": 24, "unit": "px" }, "$type": "dimension" },
"xl": { "$value": { "value": 32, "unit": "px" }, "$type": "dimension" },
"2xl": { "$value": { "value": 48, "unit": "px" }, "$type": "dimension" }
},
"typography": {
"font-family": {
"base": { "$value": "Inter", "$type": "fontFamily" },
"heading": { "$value": "Inter", "$type": "fontFamily" },
"mono": { "$value": "JetBrains Mono", "$type": "fontFamily" }
},
"font-size": {
"xs": { "$value": { "value": 12, "unit": "px" }, "$type": "dimension" },
"sm": { "$value": { "value": 14, "unit": "px" }, "$type": "dimension" },
"md": { "$value": { "value": 16, "unit": "px" }, "$type": "dimension" },
"lg": { "$value": { "value": 20, "unit": "px" }, "$type": "dimension" },
"xl": { "$value": { "value": 24, "unit": "px" }, "$type": "dimension" },
"2xl": { "$value": { "value": 32, "unit": "px" }, "$type": "dimension" }
},
"font-weight": {
"regular": { "$value": 400, "$type": "fontWeight" },
"medium": { "$value": 500, "$type": "fontWeight" },
"semibold": { "$value": 600, "$type": "fontWeight" },
"bold": { "$value": 700, "$type": "fontWeight" }
},
"line-height": {
"tight": { "$value": 1.2, "$type": "number" },
"normal": { "$value": 1.5, "$type": "number" },
"relaxed": { "$value": 1.75, "$type": "number" }
}
},
"radius": {
"none": { "$value": { "value": 0, "unit": "px" }, "$type": "dimension" },
"sm": { "$value": { "value": 4, "unit": "px" }, "$type": "dimension" },
"md": { "$value": { "value": 8, "unit": "px" }, "$type": "dimension" },
"lg": { "$value": { "value": 16, "unit": "px" }, "$type": "dimension" },
"full": { "$value": { "value": 9999, "unit": "px" }, "$type": "dimension" }
},
"shadow": {
"sm": {
"$value": {
"color": "#00000026",
"offsetX": { "value": 0, "unit": "px" },
"offsetY": { "value": 1, "unit": "px" },
"blur": { "value": 3, "unit": "px" },
"spread": { "value": 0, "unit": "px" }
},
"$type": "shadow"
}
}
}*Note on aliases: The syntax {color.brand.primary} allows creating semantic tokens that reference primitive tokens. This is essential for supporting light/dark themes without duplicating values.
This hierarchical, typed format makes it easy to interpret and transform tokens into platform-specific code.
04 TRANSFORMING TOKENS INTO FLUTTER CODE AND INTEGRATING THEM INTO THE FLUTTER THEME
- Style Dictionary: Node.js CLI
Multi-platform enterprise project, maximum flexibility - figma2flutter: Dart pub package
Flutter-only project, native integration with Tokens Studio - design_tokens_builder: Dart pub (build_runner)
Material 3 integration, automatic ThemeData generation - Script Dart custom: Dart script
Proprietary JSON format or very specific output
Style Dictionary
Style Dictionary is the standard tool, compatible with the W3C 2025.10 specification.
SETUP
npm install -D style-dictionaryProject structure
design-tokens/
├── tokens/
│ ├── color.json
│ ├── spacing.json
│ ├── typography.json
│ └── radius.json
├── config.js
└── package.jsonTo tell Style Dictionary what to produce, you need to define a configuration file.
Get Aurora Demarcus’s stories in your inbox
Join Medium for free to get updates from this writer.
EXAMPLE — config.js
const StyleDictionary = require('style-dictionary');
// ─── Custom Transform: color HEX → Flutter Color(0xFF...) ───────────────────
StyleDictionary.registerTransform({
name: 'color/flutter',
type: 'value',
matcher: (token) => token.$type === 'color' || token.type === 'color',
transformer: (token) => {
const hex = token.value.replace('#', '');
if (hex.length === 3) {
const r = hex[0].repeat(2);
const g = hex[1].repeat(2);
const b = hex[2].repeat(2);
return `const Color(0xFF${r}${g}${b})`;
}
if (hex.length === 8) {
const aa = hex.slice(6, 8);
const rgb = hex.slice(0, 6);
return `const Color(0x${aa}${rgb})`;
}
return `const Color(0xFF${hex.toUpperCase()})`;
}
});
// ─── Custom Transform: px dimension → Dart double ───────────────────────────
StyleDictionary.registerTransform({
name: 'dimension/flutter',
type: 'value',
matcher: (token) => token.$type === 'dimension' || token.type === 'dimension',
transformer: (token) => {
const raw = token.value;
if (typeof raw === 'object' && raw.value !== undefined) {
return `${parseFloat(raw.value)}`;
}
return `${parseFloat(String(raw).replace('px', ''))}`;
}
});
// ─── Custom Transform: fontWeight number → Dart FontWeight ──────────────────
StyleDictionary.registerTransform({
name: 'fontWeight/flutter',
type: 'value',
matcher: (token) => token.$type === 'fontWeight',
transformer: (token) => {
const w = token.value;
const map = {
100: 'FontWeight.w100', 200: 'FontWeight.w200',
300: 'FontWeight.w300', 400: 'FontWeight.w400',
500: 'FontWeight.w500', 600: 'FontWeight.w600',
700: 'FontWeight.w700', 800: 'FontWeight.w800',
900: 'FontWeight.w900',
};
return map[w] || `FontWeight.w${w}`;
}
});
// ─── Custom Transform Group ──────────────────────────────────────────────────
StyleDictionary.registerTransformGroup({
name: 'flutter/custom',
transforms: [
'attribute/cti',
'name/cti/camel',
'color/flutter',
'dimension/flutter',
'fontWeight/flutter',
]
});
// ─── Custom Format: generate Dart file ──────────────────────────────────────
StyleDictionary.registerFormat({
name: 'flutter/class',
formatter: ({ dictionary, options }) => {
const className = options.className || 'AppTokens';
const imports = options.imports || [];
const importLines = imports.map(i => `import '${i}';`).join('\n');
const tokens = dictionary.allTokens
.map((token) => {
const type = inferDartType(token);
return ` static const ${type} ${token.name} = ${token.value};`;
})
.join('\n');
return [
'// GENERATED FILE -- DO NOT EDIT MANUALLY',
`// Generated by Style Dictionary on ${new Date().toISOString().split('T')[0]}`,
'',
"import 'package:flutter/material.dart';",
importLines,
'',
`abstract final class ${className} {`,
tokens,
'}',
'',
].join('\n');
}
});
function inferDartType(token) {
switch (token.$type || token.type || token.attributes?.category) {
case 'color': return 'Color';
case 'dimension': return 'double';
case 'fontFamily': return 'String';
case 'fontWeight': return 'FontWeight';
case 'number': return 'double';
default: return 'dynamic';
}
}
// ─── Main configuration ──────────────────────────────────────────────────────
module.exports = {
source: ['tokens/**/*.json'],
platforms: {
flutter: {
transformGroup: 'flutter/custom',
buildPath: '../lib/design_system/generated/',
files: [
{
destination: 'app_colors.dart',
format: 'flutter/class',
options: { className: 'AppColors' },
filter: (token) => (token.$type || token.type) === 'color',
},
{
destination: 'app_spacing.dart',
format: 'flutter/class',
options: { className: 'AppSpacing' },
filter: (token) => token.path[0] === 'spacing',
},
{
destination: 'app_typography.dart',
format: 'flutter/class',
options: { className: 'AppTypography' },
filter: (token) => token.path[0] === 'typography',
},
{
destination: 'app_radius.dart',
format: 'flutter/class',
options: { className: 'AppRadius' },
filter: (token) => token.path[0] === 'radius',
},
]
}
}
};Build command
npx style-dictionary build --config config.jsGenerated output
// GENERATED FILE -- DO NOT EDIT MANUALLY
// Generated by Style Dictionary on 2025-10-28
import 'package:flutter/material.dart';
abstract final class AppColors {
static const Color brandPrimary = const Color(0xFF007AFF);
static const Color brandPrimaryHover = const Color(0xFF005EC4);
static const Color brandSecondary = const Color(0xFF5AC8FA);
static const Color neutral100 = const Color(0xFFF5F5F5);
static const Color neutral500 = const Color(0xFF9E9E9E);
static const Color neutral900 = const Color(0xFF212121);
static const Color semanticError = const Color(0xFFFF3B30);
static const Color semanticSuccess = const Color(0xFF34C759);
static const Color semanticWarning = const Color(0xFFFFCC00);
}EXAMPLE — Project structure
lib/
└── design_system/
├── generated/ ← TOOL OUTPUT
│ ├── app_colors.dart
│ ├── app_spacing.dart
│ ├── app_typography.dart
│ └── app_radius.dart
├── tokens/ ← ABSTRACTION LAYER
│ ├── color_tokens.dart
│ ├── typography_tokens.dart
│ └── spacing_tokens.dart
├── theme/
│ ├── app_theme.dart
│ ├── app_theme_extension.dart
│ └── app_text_theme.dart
└── design_system.dart ← barrel fileSemantic Tokens
The generated files contain primitive tokens (e.g., brandPrimary = Color(0xFF007AFF)). It is good practice to create a semantic layer on top of them:
lib/design_system/tokens/color_tokens.dart
import '../generated/app_colors.dart';
/// Semantic tokens: they describe the ROLE, not the value.
/// This file can be edited manually; app_colors.dart cannot.
abstract final class ColorTokens {
// Actions
static const Color actionPrimary = AppColors.brandPrimary;
static const Color actionPrimaryHover = AppColors.brandPrimaryHover;
static const Color actionDestructive = AppColors.semanticError;
// Surface
static const Color surfaceDefault = AppColors.neutral100;
static const Color surfaceInverse = AppColors.neutral900;
// Text
static const Color textDefault = AppColors.neutral900;
static const Color textSubtle = AppColors.neutral500;
static const Color textOnPrimary = AppColors.neutral100;
// Feedback
static const Color feedbackError = AppColors.semanticError;
static const Color feedbackSuccess = AppColors.semanticSuccess;
static const Color feedbackWarning = AppColors.semanticWarning;
}Integration with Material 3 and ThemeData
app_theme.dart
import 'package:flutter/material.dart';
import '../generated/app_colors.dart';
import '../generated/app_typography.dart';
import '../tokens/color_tokens.dart';
import 'app_theme_extension.dart';
import 'app_text_theme.dart';
abstract final class AppTheme {
static ThemeData get light => _build(Brightness.light);
static ThemeData get dark => _build(Brightness.dark);
static ThemeData _build(Brightness brightness) {
final isLight = brightness == Brightness.light;
final colorScheme = ColorScheme(
brightness: brightness,
primary: AppColors.brandPrimary,
onPrimary: AppColors.neutral100,
primaryContainer: AppColors.brandSecondary,
onPrimaryContainer: AppColors.neutral900,
secondary: AppColors.brandSecondary,
onSecondary: AppColors.neutral900,
secondaryContainer: AppColors.neutral100,
onSecondaryContainer: AppColors.neutral900,
error: AppColors.semanticError,
onError: AppColors.neutral100,
surface: isLight ? AppColors.neutral100 : AppColors.neutral900,
onSurface: isLight ? AppColors.neutral900 : AppColors.neutral100,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
textTheme: AppTextTheme.build(),
extensions: [AppThemeExtension.from(brightness)],
);
}
}ThemeExtension for Custom Tokens
Material 3 does not cover all tokens (e.g., spacing, custom radius). Using ThemeExtension is the best practice for exposing them:
app_theme_extension.dart
import 'package:flutter/material.dart';
import '../generated/app_colors.dart';
import '../generated/app_spacing.dart';
import '../generated/app_radius.dart';
@immutable
class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
const AppThemeExtension({
required this.surfaceCard,
required this.textSubtle,
required this.spacingXs,
required this.spacingSm,
required this.spacingMd,
required this.spacingLg,
required this.spacingXl,
required this.radiusSm,
required this.radiusMd,
required this.radiusLg,
});
final Color surfaceCard;
final Color textSubtle;
final double spacingXs;
final double spacingSm;
final double spacingMd;
final double spacingLg;
final double spacingXl;
final double radiusSm;
final double radiusMd;
final double radiusLg;
factory AppThemeExtension.from(Brightness brightness) {
final isLight = brightness == Brightness.light;
return AppThemeExtension(
surfaceCard: isLight ? AppColors.neutral100 : AppColors.neutral900,
textSubtle: AppColors.neutral500,
spacingXs: AppSpacing.xs,
spacingSm: AppSpacing.sm,
spacingMd: AppSpacing.md,
spacingLg: AppSpacing.lg,
spacingXl: AppSpacing.xl,
radiusSm: AppRadius.sm,
radiusMd: AppRadius.md,
radiusLg: AppRadius.lg,
);
}
@override
AppThemeExtension copyWith({
Color? surfaceCard,
Color? textSubtle,
double? spacingXs,
double? spacingSm,
double? spacingMd,
double? spacingLg,
double? spacingXl,
double? radiusSm,
double? radiusMd,
double? radiusLg,
}) {
return AppThemeExtension(
surfaceCard: surfaceCard ?? this.surfaceCard,
textSubtle: textSubtle ?? this.textSubtle,
spacingXs: spacingXs ?? this.spacingXs,
spacingSm: spacingSm ?? this.spacingSm,
spacingMd: spacingMd ?? this.spacingMd,
spacingLg: spacingLg ?? this.spacingLg,
spacingXl: spacingXl ?? this.spacingXl,
radiusSm: radiusSm ?? this.radiusSm,
radiusMd: radiusMd ?? this.radiusMd,
radiusLg: radiusLg ?? this.radiusLg,
);
}
@override
AppThemeExtension lerp(AppThemeExtension? other, double t) {
if (other is! AppThemeExtension) return this;
return AppThemeExtension(
surfaceCard: Color.lerp(surfaceCard, other.surfaceCard, t)!,
textSubtle: Color.lerp(textSubtle, other.textSubtle, t)!,
spacingXs: lerpDouble(spacingXs, other.spacingXs, t)!,
spacingSm: lerpDouble(spacingSm, other.spacingSm, t)!,
spacingMd: lerpDouble(spacingMd, other.spacingMd, t)!,
spacingLg: lerpDouble(spacingLg, other.spacingLg, t)!,
spacingXl: lerpDouble(spacingXl, other.spacingXl, t)!,
radiusSm: lerpDouble(radiusSm, other.radiusSm, t)!,
radiusMd: lerpDouble(radiusMd, other.radiusMd, t)!,
radiusLg: lerpDouble(radiusLg, other.radiusLg, t)!,
);
}
}
/// Extension method to access tokens from the BuildContext
extension AppThemeExtensionX on BuildContext {
AppThemeExtension get tokens =>
Theme.of(this).extension<AppThemeExtension>()!;
}Widget Access
// Access via extension — readable and type-safe
@override
Widget build(BuildContext context) {
final tokens = context.tokens;
return Card(
color: tokens.surfaceCard,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(tokens.radiusMd),
),
child: Padding(
padding: EdgeInsets.all(tokens.spacingMd),
child: Text(
'Title',
style: Theme.of(context).textTheme.titleLarge,
),
),
);
}05 BENEFITS IN FLUTTER PROJECTS
Adopting Design Tokens in Flutter projects offers numerous advantages:
- Brand Consistency: Maintains a uniform brand image across all platforms and products.
- Consistency: Ensures the user interface is coherent across all screens and components, respecting brand identity.
- Single Source of Truth: Designers and developers refer to the same definition of design values, eliminating ambiguity.
- Maintainability: Global updates to design values (e.g., a brand color change) can be made in a single location and propagated automatically, drastically reducing maintenance effort.
- Collaboration: Facilitates collaboration between design and development teams by providing a common language and shared tools.
- Scalability: Allows the design system to grow simply with product complexity, adding new tokens and variants without breaking what already exists.
- Development Efficiency: Developers spend less time guessing design values or fixing discrepancies, and can focus on application logic.
06 CHALLENGES AND TRADE-OFFS
Despite the many advantages, implementing Design Tokens also presents some challenges and trade-offs:
- Initial Cost: The initial setup of a Design Tokens system — including token definition, JSON export, and the code generation pipeline — requires an investment of time and resources.
- Pipeline Complexity: Automating the pipeline can become complex, especially in projects with specific requirements or a large number of platforms.
- Learning Curve: Designers and developers must become familiar with the concept of Design Tokens and the associated tools.
- Maintaining Standards: It is crucial to adhere to a standard (such as the W3C one) and keep token documentation up to date to avoid confusion.
- Tooling Overhead: It may be necessary to introduce new tools (Figma plugins, code generators) into the workflow, potentially adding complexity.
- Variant Management: Managing token variants (e.g., light/dark themes, different sizes for tablet/mobile) can require a more sophisticated JSON structure and more complex code generation logic.
07 CONCLUSIONS
Design Tokens represent an essential practice for building robust and scalable design systems. By adopting the emerging W3C standards, organizations can ensure that their design decisions constitute a single source of truth, consistent across all platforms. For Flutter projects, this translates into visually consistent applications that are easier to maintain and enable a more efficient development workflow. Although the initial investment is significant, the long-term benefits in terms of brand consistency, efficiency, and scalability make Design Tokens an indispensable component of a modern design system.
We should think of Design Tokens as the APIs that enable communication between designers and developers.
Sources
- Design Tokens Technical Reports 2025.10
- 99% of the Design Tokens in the world of Design Systems share this mistake! — Medium ☕
by Aurora, Mobile developer
if you liked this article, follow @overapp or visit our website www.overapp.it











