Design a token system that generates consistent, platform-appropriate values for web CSS, iOS Swift, and Android Kotlin from a single source of truth in Figma.
Design a token system that generates consistent, platform-appropriate values for web CSS, iOS Swift, and Android Kotlin from a single source of truth in Figma.
The same design token needs different numeric values across platforms because unit systems differ fundamentally. Web uses rem (relative to root font size, typically 16px), iOS uses points (pt, device-independent at 72pt/inch), Android uses density-independent pixels (dp, 1dp = 1px on 160dpi screens). A 16px spacing value translates to: 1rem on web, 12pt on iOS (16px × 0.75), and 16dp on Android (at baseline density).
Style Dictionary transforms handle this conversion automatically:
1// style-dictionary.config.mjs
2StyleDictionary.registerTransform({
3 name: 'size/pxToRem',
4 type: 'value',
5 matcher: t => t.type === 'spacing' || t.type === 'sizing',
6 transformer: t => `${parseFloat(t.original.value) / 16}rem`,
7});
8
9StyleDictionary.registerTransform({
10 name: 'size/pxToPt',
11 type: 'value',
12 matcher: t => t.type === 'spacing' || t.type === 'sizing',
13 transformer: t => `${parseFloat(t.original.value) * 0.75}`,
14});
15
16StyleDictionary.registerTransform({
17 name: 'size/pxToDp',
18 type: 'value',
19 matcher: t => t.type === 'spacing' || t.type === 'sizing',
20 // Android dp at 1x density equals px — no conversion needed
21 transformer: t => `${parseFloat(t.original.value)}dp`,
22});Style Dictionary's ios-swift transform group generates Swift-compatible output. Create custom extensions for SwiftUI and UIKit compatibility:
1// Register a custom Swift format
2StyleDictionary.registerFormat({
3 name: 'swift/color-extension',
4 formatter: ({ dictionary }) => {
5 const colors = dictionary.allTokens.filter(t => t.type === 'color');
6 const swiftCases = colors
7 .map(t => ` static let ${toCamelCase(t.name)} = Color(hex: "${t.value}")`)
8 .join('\n');
9
10 return [
11 'import SwiftUI',
12 '',
13 '// Auto-generated. Do not edit manually.',
14 '// Source: Design System Token Pipeline',
15 'extension Color {',
16 swiftCases,
17 '}',
18 ].join('\n');
19 },
20});The generated Swift file looks like:
1// Generated: packages/tokens/dist/ios/Colors.swift
2import SwiftUI
3
4extension Color {
5 static let interactivePrimary = Color(hex: "#3B82F6")
6 static let surfaceDefault = Color(hex: "#FFFFFF")
7 static let textPrimary = Color(hex: "#111827")
8 static let statusError = Color(hex: "#DC2626")
9}
10
11// Usage in SwiftUI
12Button("Save") { }
13 .background(Color.interactivePrimary)
14 .foregroundColor(Color.surfaceDefault)Include dark mode variants using Color asset catalog generation, where each color has light and dark entries.
Android uses XML resource files for colors, dimensions, and themes. Style Dictionary generates these from the same token source:
1<!-- Generated: packages/tokens/dist/android/values/colors.xml -->
2<?xml version="1.0" encoding="utf-8"?>
3<!-- Auto-generated. Do not edit manually. -->
4<resources>
5 <color name="color_interactive_primary">#FF3B82F6</color>
6 <color name="color_surface_default">#FFFFFFFF</color>
7 <color name="color_text_primary">#FF111827</color>
8 <color name="color_status_error">#FFDC2626</color>
9</resources>1<!-- Generated: values-night/colors.xml (dark mode) -->
2<resources>
3 <color name="color_interactive_primary">#FF60A5FA</color>
4 <color name="color_surface_default">#FF030712</color>
5 <color name="color_text_primary">#FFF9FAFB</color>
6</resources>Android color values include an alpha channel prefix (FF = fully opaque). Style Dictionary's android/colors format handles this transformation automatically. For dimensions:
1<!-- Generated: values/dimens.xml -->
2<resources>
3 <dimen name="spacing_1">4dp</dimen>
4 <dimen name="spacing_4">16dp</dimen>
5 <dimen name="spacing_8">32dp</dimen>
6 <dimen name="radius_component">6dp</dimen>
7</resources>Typography tokens are the trickiest to cross-platform because line height conventions differ significantly. Web uses unitless multipliers (1.5 = 150% of font size), iOS uses absolute point values, Android uses scale-independent pixels (sp, same as dp but respects user font size preferences).
Structure typography tokens to encode all needed values:
1// tokens/typography.json
2{
3 "typography": {
4 "body": {
5 "size": { "value": "16", "type": "fontSizes" },
6 "lineHeight": { "value": "24", "type": "lineHeights" },
7 "weight": { "value": "400", "type": "fontWeights" },
8 "letterSpacing": { "value": "0", "type": "letterSpacing" }
9 }
10 }
11}Custom transforms produce platform-appropriate values:
font-size: 1rem; line-height: 1.5; (24/16)Font.system(size: 16, weight: .regular) with lineSpacing: 8 (24-16)<dimen name="text_size_body">16sp</dimen> with lineHeight in dpDocument the platform-specific lineHeight calculation in your token taxonomy so designers understand why the same token produces different numbers per platform.
Platform discrepancies creep in when transforms are wrong or when manual overrides diverge from the generated output. Build a validation dashboard:
1// scripts/validate-cross-platform.ts
2interface TokenValidation {
3 tokenName: string;
4 web: string;
5 ios: string;
6 android: string;
7 isConsistent: boolean;
8 discrepancy?: string;
9}
10
11async function validateCrossPlatform(): Promise<TokenValidation[]> {
12 const webTokens = parseCSS('dist/web/tokens.css');
13 const iosTokens = parseSwift('dist/ios/Colors.swift');
14 const androidTokens = parseXML('dist/android/values/colors.xml');
15
16 const validations: TokenValidation[] = [];
17
18 for (const [name, webValue] of Object.entries(webTokens)) {
19 const iosValue = iosTokens[toCamelCase(name)];
20 const androidValue = androidTokens[toSnakeCase(name)];
21
22 // Convert values to a common comparison format
23 const webHex = cssColorToHex(webValue);
24 const iosHex = swiftColorToHex(iosValue);
25 const androidHex = androidColorToHex(androidValue);
26
27 const isConsistent = webHex === iosHex && webHex === androidHex;
28 validations.push({
29 tokenName: name, web: webHex, ios: iosHex ?? 'MISSING',
30 android: androidHex ?? 'MISSING', isConsistent,
31 discrepancy: isConsistent ? undefined :
32 `Web: ${webHex}, iOS: ${iosHex}, Android: ${androidHex}`,
33 });
34 }
35
36 return validations;
37}
38
39const results = await validateCrossPlatform();
40const failures = results.filter(r => !r.isConsistent);
41if (failures.length > 0) {
42 console.error('Cross-platform token discrepancies:', failures);
43 process.exit(1);
44}Add iOS and Android platforms to your Style Dictionary configuration and run a build. Compare the generated Swift color values against your current iOS codebase and document any discrepancies for the next design system sync.
Your progress is saved in this browser, on this device only.