Style Dictionary's Flutter support has been broken for years. I built tokensync — a CLI that generates CSS and Flutter ThemeData from one tokens.json file, then verifies they match numerically.
Your designer just updated the brand color.
You open your CSS file. Update --color-brand-500. Then you open your Flutter file. Update AppTheme._lightColors.primary. Then you grep for anywhere else it might appear. Then you do the same for dark mode. Then you hope you got them all.
Three weeks later a designer screenshots both apps side by side. The web button is #5C6BC0. The mobile button is #5B6BC0.
Off by one digit. Nobody noticed. It shipped.
If you maintain both a React web app and a Flutter mobile app, this is a design token sync problem — and it costs teams 6–20 hours every time tokens change.
Why existing design token tools don't solve this
Style Dictionary is the industry standard for design token transformation. It's genuinely great for CSS. But its Flutter support produces flat key-value constants — not the ThemeData, ColorScheme, or TextTheme that Flutter apps actually need:
// What Style Dictionary gives you for Flutter:
static const double typographyDisplayFontSize = 48.0;
static const double typographyDisplayFontWeight = 700;
// That's it. You still write TextStyle yourself.
GitHub issues requesting proper Flutter support have been open since 2022 with no resolution. Most teams end up writing a custom script. It works until the first rebrand, then breaks.
The other pain point is letter-spacing conversion. CSS letter-spacing: -0.02em at font-size: 48px should become Flutter letterSpacing: -0.96. Every team figures this out independently. Every team gets it wrong at least once.
Introducing tokensync: one source of truth for React and Flutter design tokens
tokensync is an open-source CLI that reads a single tokens.json file in W3C DTCG format and generates:
-
tokens.css— CSS custom properties with:root(light) and[data-theme="dark"](dark) selectors -
app_theme.dart— complete FlutterThemeData,ColorScheme,TextTheme, andTextStyle -
tokens.ts— typed TypeScript constants for shared logic
Then it runs a parity check — numerically comparing every token value between the CSS and Dart outputs to catch drift before it ships.
npx tokensync init # scaffold config + tokens.json
npx tokensync build # generate all platform outputs
npx tokensync check # verify React and Flutter values match
What the generated Flutter output looks like
Given this design token:
{
"typography": {
"display": {
"$type": "typography",
"$value": {
"fontFamily": "Inter",
"fontSize": "48px",
"fontWeight": 700,
"lineHeight": "1.1",
"letterSpacing": "-0.02em"
}
}
}
}
tokensync generates idiomatic Flutter ThemeData:
static const TextTheme _textTheme = TextTheme(
displayLarge: TextStyle(
fontFamily: 'Inter',
fontSize: 48.0,
fontWeight: FontWeight.w700,
height: 1.1,
letterSpacing: -0.96, // ← -0.02em × 48px, correctly converted
),
);
And the matching CSS:
:root {
--typography-display-font-family: Inter;
--typography-display-font-size: 48px;
--typography-display-font-weight: 700;
--typography-display-line-height: 1.1;
--typography-display-letter-spacing: -0.02em;
}
The generated Dart file passes dart analyze with zero errors out of the box.
How the parity checker works
After building, tokensync compares every token value between platforms:
Parity: web vs flutter
11 tokens · 11 passed
✓ Parity check passed
It normalizes values before comparing: em to px, named font weights ("SemiBold" → 600), 0em and 0.0 both treated as zero. If anything diverges, it reports the exact token name and delta.
For CI, add this to your pipeline:
tokensync check --ci # exits with code 1 if CSS and Dart diverge
Mismatches block merges. No more #5C6BC0 vs #5B6BC0 in production.
Dark mode with a single token definition
Define your semantic tokens with light and dark values:
{
"semantic": {
"$modes": {
"light": { "color": { "primary": { "$value": "{color.brand.500}" } } },
"dark": { "color": { "primary": { "$value": "{color.brand.100}" } } }
},
"color": {
"primary": { "$type": "color", "$value": "{color.brand.500}" }
}
}
}
tokensync generates both :root / [data-theme="dark"] blocks in CSS and both ThemeData.light() / ThemeData.dark() in Flutter — from the same source of truth.
Figma integration
If your design tokens live in Figma Variables (Professional plan):
FIGMA_ACCESS_TOKEN=your_token FIGMA_FILE_KEY=your_key tokensync pull
On the free Figma plan, use the Styles API instead:
tokensync pull --figma-api styles
Both write a tokens.json ready for tokensync build.
Tokens Studio exports are also supported via a built-in adapter.
How it compares to Style Dictionary
| Feature | Style Dictionary | tokensync |
|---|---|---|
| CSS custom properties | ✓ | ✓ |
Flutter ColorScheme (light + dark) |
✗ | ✓ |
Flutter TextTheme with named slots |
✗ | ✓ |
TextStyle with correct letterSpacing
|
✗ | ✓ |
| em → px letter-spacing conversion | ✗ | ✓ |
Named font weights ("SemiBold" → FontWeight.w600) |
✗ | ✓ |
| Cross-platform parity check | ✗ | ✓ |
| Figma Variables pull | ✗ | ✓ |
Getting started
# Install globally
npm install -g tokensync
# Or run with npx
npx tokensync init && npx tokensync build
The init command scaffolds a tokensync.config.ts and a sample tokens.json with colors, spacing, and typography — including light/dark modes. Running build generates all three platform outputs and runs the parity check.
Requirements: Node.js 18+. Zero runtime dependencies. MIT license.
GitHub: [https://github.com/rahulpatwa1303/tokensync]
Current status and what's next
This is v0.1.0. It handles the token types I've encountered in real projects: color, dimension, typography, shadow, number. The generated Dart is idiomatic and passes static analysis.
Planned for v0.2.0: React Native formatter, oklch color support, watch mode improvements.
Do you have this problem?
If you ship both React and Flutter, I'd genuinely like to know how you handle design token sync today. Manual copy-paste? A custom script? A SaaS tool? Something in your CI?
Drop a comment — trying to understand whether this pain is widespread or specific to how my team works.
Top comments (0)