From Figma to Flutter and Web: Structuring Multi-Platform Design Tokens with Style Dictionary
The architecture behind synchronizing typography, spacing, and semantic color scales across React, Flutter, and iOS from a single source of truth without manual copy-paste.
Every growing product team goes through the same painful evolution with design consistency:
- Phase 1 (The Wild West): Engineers hard-code hex colors and pixel values directly into CSS and Flutter widgets. You end up with 14 distinct shades of gray and 6 slightly different primary blues across your apps.
- Phase 2 (The Static Config): The team creates a
colors.tson web, acolors.dartin Flutter, and aColors.swifton iOS. But keeping them aligned requires a designer posting hex codes in a Slack channel, followed by three developers manually copy-pasting strings into three repositories. Within two months, the platforms drift apart. - Phase 3 (Automated Token Architecture): Design decisions live in a single, platform-agnostic token repository. Changes in Figma trigger an automated CI pipeline that parses, transforms, and publishes versioned SDK packages across Web, iOS, Android, and Flutter in under four minutes.
Here is how we built our multi-platform token pipeline using Style Dictionary v4, JSON Schema ASTs, and automated platform formatters.
The Token Hierarchy: Primitives vs. Semantics vs. Components
The biggest mistake teams make when setting up design tokens is exposing raw values directly to component libraries.
A resilient design system must structure tokens into three distinct tiers:
TIER 1: GLOBAL / PRIMITIVE TOKENS (Raw values)
└── color.palette.emerald.500: "#1B5448"
└── dimension.spacing.base: 8px
│ (Referenced by)
▼
TIER 2: SEMANTIC / CONTEXTUAL TOKENS (Intent & Role)
└── color.surface.primary.dark: "{color.palette.emerald.900}"
└── color.surface.primary.light: "{color.palette.emerald.50}"
└── color.text.action: "{color.palette.emerald.500}"
│ (Referenced by)
▼
TIER 3: COMPONENT TOKENS (Specific element bindings)
└── button.primary.background.default: "{color.surface.primary.dark}"
└── button.primary.border.radius: "{dimension.radii.lg}"
Why this separation? Because when marketing decides that your primary brand green should shift from forest emerald to electric mint, you change exactly one primitive token. The semantic and component tiers cascade automatically across dark mode and light mode without rewriting a single component template.
The Pipeline Architecture: From JSON to Native Code
[ Figma Variables / Tokens Studio ]
│
(Export Git PR)
│
▼
[ tokens/src/**/*.json ] (Single Source of Truth)
│
▼
[ Style Dictionary Transformer Engine ]
├── AST validation with Zod
├── Color format conversion (HEX -> OKLCH -> Color(0xFF...))
└── Dimension conversion (px -> rem -> dp -> pt)
│
┌───────────┼───────────┬───────────┐
▼ ▼ ▼ ▼
[ CSS/Tailwind ] [ Dart/Flutter ] [ Swift/iOS ] [ Kotlin/Compose ]
:root { ... } class AppTokens struct AppColor val Primary500
Customizing Formatters for Flutter and Web
Style Dictionary takes platform-agnostic JSON tokens and feeds them through configurable formatters:
// style-dictionary.config.js
import StyleDictionary from 'style-dictionary';
export default {
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'build/web/',
files: [{
destination: 'tokens.css',
format: 'css/variables',
options: { outputReferences: true }
}]
},
flutter: {
transformGroup: 'flutter',
buildPath: 'build/flutter/lib/',
files: [{
destination: 'design_tokens.dart',
format: 'flutter/class.dart',
className: 'AppDesignTokens'
}]
},
ios: {
transformGroup: 'ios-swift',
buildPath: 'build/ios/',
files: [{
destination: 'StyleTokens.swift',
format: 'ios-swift/class.swift',
className: 'StyleTokens'
}]
}
}
};
The Output Generated for Flutter (design_tokens.dart):
// Generated by Style Dictionary - DO NOT EDIT MANUALLY
import 'package:flutter/material.dart';
abstract class AppDesignTokens {
static const colorPrimary500 = Color(0xFF1B5448);
static const colorPrimaryAccent = Color(0xFFADE1CD);
static const spaceSm = 8.0;
static const spaceMd = 16.0;
static const radiusLg = 12.0;
}
The Output Generated for Web (tokens.css):
/* Generated by Style Dictionary - DO NOT EDIT MANUALLY */
:root {
--color-primary-500: #1b5448;
--color-primary-accent: #ade1cd;
--space-sm: 0.5rem;
--space-md: 1.0rem;
--radius-lg: 0.75rem;
}
Quantifiable Engineering Impact
Transitioning to automated token synchronization transformed our release cadence:
| Metric | Before (Manual Coordination) | After (Automated Token CI) |
|---|---|---|
| Time to Propagate Design Change | ~3.5 working days | 4 minutes (Automated CI PR) |
| Cross-Platform Inconsistencies | 42 logged QA visual tickets / quarter | 0 (Mathematical guarantee) |
| Developer Onboarding on Spacing | ”Check Figma inspect mode” | Autocompleted strongly typed constants |
| Dark Mode Implementation Time | 4 weeks of manual style overrides | 2 days (Swapping semantic token map) |
Best Practices Before You Begin
- Enforce naming conventions strictly: Use kebab-case or dot-notation (
category.component.variant.state). Never allow arbitrary names likemyBlueorheaderBigNew. - Never import primitives into UI components: Prevent developers from using
--color-palette-blue-500. Enforce linting rules that require semantic tokens like--color-interactive-primary. - Automate visual regression in CI: Run automated Playwright or Flutter golden screenshot tests on your component library whenever a token PR merges. If bumping a margin accidentally wraps a button label on mobile, your CI catches it before release.
Engineer specializing in Android, iOS, Flutter, and web applications. Focuses on scalable software architectures, performance optimization, and developer productivity tools.