TLDR
Design tokens for multi-platform apps solve the problem of maintaining consistent brand identity across iOS, Android, and web by defining design decisions once and transforming them into platform-specific formats. A single source of truth in JSON prevents platform drift, ensures brand consistency, and reduces the maintenance burden of updating colors, typography, and spacing values across multiple codebases.
Key takeaways:
- Use JSON as your platform-agnostic source format following the W3C Design Tokens specification
- Transform tokens into CSS custom properties (web), XML resources (Android), and Swift constants (iOS)
- Account for platform differences in units (px vs dp vs pt), color formats, and font handling
- Implement Style Dictionary to automate the build process from one source to multiple outputs
- For React Native projects, use JavaScript token modules that work across both platforms
Why Multi-Platform Tokens?
Modern product teams build for multiple platforms simultaneously: web apps, iOS apps, Android apps, and increasingly desktop applications. Each platform has its own code conventions, file formats, and developer expectations. Without a systematic approach, keeping these platforms visually consistent becomes a maintenance nightmare.
Brand consistency across platforms is the primary goal. When a user experiences your product on iPhone, Android, and web, they should see the same brand colors, typography hierarchy, and spacing patterns. Inconsistencies create a fragmented brand experience and signal lack of polish. Design tokens solve this by encoding these decisions in a platform-neutral format.
Single source of truth eliminates the manual copying of values across platforms. When your brand's primary color changes from #3b82f6 to #4f46e5, you update one JSON file instead of hunting through iOS Swift files, Android XML resources, and CSS stylesheets. The token system handles propagating that change to all platforms through automated builds.
Preventing platform drift is crucial for long-term maintainability. Without tokens, platforms inevitably diverge. Android uses #3b82f6 while iOS uses a slightly different #3B83F6 and web uses rgb(59, 130, 246). These are the same color, but three different representations. Over time, manual updates introduce errors, and suddenly your "blue" button is three different blues. Tokens prevent this by maintaining a single canonical definition.
Design system scaling becomes manageable with tokens. As your design system grows from 5 colors to 50, from 3 text styles to 20, and from basic spacing to a complete elevation system, manually maintaining this across platforms becomes impossible. Token-based systems scale effortlessly because the complexity lives in one place—the source file—while platform outputs are generated automatically.
Platform-Specific Token Outputs
Each platform has its own conventions for defining design values. Here's what the same token looks like in each environment.
Web: CSS Custom Properties
Modern web development uses CSS custom properties (CSS variables) for design tokens. They're defined once at the root level and referenced throughout stylesheets, with support for runtime theming and media query overrides.
/* tokens.css - generated for web */
:root {
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-background: #ffffff;
--color-text-primary: #0f172a;
--font-size-base: 16px;
--font-size-lg: 20px;
--font-weight-regular: 400;
--font-weight-semibold: 600;
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
}These tokens are then referenced in component styles:
.button-primary {
background-color: var(--color-primary);
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--color-background);
}Android: XML Resources
Android uses XML resource files for colors, dimensions, and other values. These live in res/values/ and are accessed programmatically via the R class or directly in XML layouts.
<!-- colors.xml - generated for Android -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3B82F6</color>
<color name="colorPrimaryHover">#2563EB</color>
<color name="colorBackground">#FFFFFF</color>
<color name="colorTextPrimary">#0F172A</color>
</resources><!-- dimens.xml - generated for Android -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="spacing_xs">4dp</dimen>
<dimen name="spacing_sm">8dp</dimen>
<dimen name="spacing_md">16dp</dimen>
<dimen name="spacing_lg">24dp</dimen>
<dimen name="font_size_base">16sp</dimen>
<dimen name="font_size_lg">20sp</dimen>
</resources>Used in layouts and code:
<!-- In XML layout -->
<Button
android:background="@color/colorPrimary"
android:paddingHorizontal="@dimen/spacing_md"
android:paddingVertical="@dimen/spacing_sm"
android:textSize="@dimen/font_size_base"
android:textColor="@color/colorBackground" />iOS: Swift Constants
iOS projects typically use Swift constants organized in structs or enums. These provide type-safe access to design tokens with autocomplete support in Xcode.
// Tokens.swift - generated for iOS
import UIKit
enum ColorTokens {
static let primary = UIColor(hex: "#3B82F6")
static let primaryHover = UIColor(hex: "#2563EB")
static let background = UIColor(hex: "#FFFFFF")
static let textPrimary = UIColor(hex: "#0F172A")
}
enum SpacingTokens {
static let xs: CGFloat = 4
static let sm: CGFloat = 8
static let md: CGFloat = 16
static let lg: CGFloat = 24
}
enum FontTokens {
static let base = UIFont.systemFont(ofSize: 16, weight: .regular)
static let large = UIFont.systemFont(ofSize: 20, weight: .regular)
static let baseSemibold = UIFont.systemFont(ofSize: 16, weight: .semibold)
}Used in SwiftUI or UIKit code:
// SwiftUI
Button("Click me") {
// action
}
.padding(.horizontal, SpacingTokens.md)
.padding(.vertical, SpacingTokens.sm)
.background(Color(ColorTokens.primary))
.foregroundColor(Color(ColorTokens.background))
.font(Font(FontTokens.baseSemibold))Platform Differences
While the goal is consistency, platforms have inherent differences that token systems must handle.
Units: Pixels, DP, and Points
Web uses pixels (px) as the base unit. CSS pixels are resolution-independent—a 16px font appears the same physical size on retina and non-retina displays because browsers handle scaling automatically.
Android uses density-independent pixels (dp) which scale based on screen density. A button with 16dp padding appears the same physical size on a low-density phone and a high-density tablet. For fonts, Android uses sp (scale-independent pixels) which respects user font size preferences.
iOS uses points (pt) which are similar to Android's dp. A 16pt spacing value maintains consistent physical size across iPhone, iPad, and different retina densities (1x, 2x, 3x).
For most design tokens, the numeric values stay the same: 16px becomes 16dp on Android and 16pt on iOS. The build tool handles appending the correct unit suffix.
Color Formats
Web supports hex, rgb, rgba, and hsl. Most token systems use hex (#3B82F6) for colors without transparency and rgba() for semi-transparent colors.
Android XML uses 8-digit hex with alpha in the format #AARRGGBB. A fully opaque blue is #FF3B82F6 (alpha channel first). Semi-transparent colors require converting web's rgba(59, 130, 246, 0.5) to Android's #803B82F6.
iOS UIColor can be initialized from hex strings but requires a helper extension. SwiftUI's Color can use hex with a custom initializer. Token build tools handle these conversions automatically.
Font Handling
Web references fonts by family name with fallback stacks: font-family: 'Inter', -apple-system, sans-serif. Custom fonts must be loaded via @font-face or font service.
Android requires font files in res/font/ and references them by filename. Font weights are separate files: inter_regular.ttf, inter_semibold.ttf. XML references fonts as @font/inter_regular.
iOS uses system fonts by default (UIFont.systemFont) or custom fonts registered in Info.plist. SwiftUI can reference custom fonts by name with .font(.custom("Inter", size: 16)).
Token systems often separate font family (a string) from font size and weight (numeric values) to handle these platform differences cleanly.
Code Example: One Source, Multiple Outputs
Here's a complete working example using Style Dictionary to transform a single token source into outputs for web, Android, and iOS.
Source: tokens.json
Define your tokens in a platform-agnostic JSON format following the W3C Design Tokens specification:
{
"color": {
"brand": {
"primary": {
"value": "#3b82f6",
"type": "color"
},
"primaryHover": {
"value": "#2563eb",
"type": "color"
}
},
"neutral": {
"white": {
"value": "#ffffff",
"type": "color"
},
"gray900": {
"value": "#0f172a",
"type": "color"
}
}
},
"fontSize": {
"base": {
"value": 16,
"type": "dimension"
},
"lg": {
"value": 20,
"type": "dimension"
}
},
"spacing": {
"xs": {
"value": 4,
"type": "dimension"
},
"sm": {
"value": 8,
"type": "dimension"
},
"md": {
"value": 16,
"type": "dimension"
}
}
}Build Config: config.js
Configure Style Dictionary to generate platform-specific outputs:
// config.js - Style Dictionary configuration
module.exports = {
source: ['tokens.json'],
platforms: {
// Web output: CSS custom properties
css: {
transformGroup: 'css',
buildPath: 'dist/web/',
files: [{
destination: 'tokens.css',
format: 'css/variables'
}]
},
// Android output: XML resources
android: {
transformGroup: 'android',
buildPath: 'dist/android/res/values/',
files: [
{
destination: 'colors.xml',
format: 'android/colors',
filter: {
type: 'color'
}
},
{
destination: 'dimens.xml',
format: 'android/dimens',
filter: {
type: 'dimension'
}
}
]
},
// iOS output: Swift constants
ios: {
transformGroup: 'ios',
buildPath: 'dist/ios/',
files: [{
destination: 'Tokens.swift',
format: 'ios-swift/class.swift',
className: 'Tokens'
}]
}
}
};Generated Output: Web (CSS)
Running style-dictionary build generates:
/* dist/web/tokens.css */
:root {
--color-brand-primary: #3b82f6;
--color-brand-primary-hover: #2563eb;
--color-neutral-white: #ffffff;
--color-neutral-gray-900: #0f172a;
--font-size-base: 16px;
--font-size-lg: 20px;
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
}Generated Output: Android (XML)
<!-- dist/android/res/values/colors.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="color_brand_primary">#3b82f6</color>
<color name="color_brand_primary_hover">#2563eb</color>
<color name="color_neutral_white">#ffffff</color>
<color name="color_neutral_gray_900">#0f172a</color>
</resources><!-- dist/android/res/values/dimens.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="font_size_base">16sp</dimen>
<dimen name="font_size_lg">20sp</dimen>
<dimen name="spacing_xs">4dp</dimen>
<dimen name="spacing_sm">8dp</dimen>
<dimen name="spacing_md">16dp</dimen>
</resources>Generated Output: iOS (Swift)
// dist/ios/Tokens.swift
import UIKit
public class Tokens {
public static let colorBrandPrimary = UIColor(red: 0.231, green: 0.510, blue: 0.965, alpha: 1.0)
public static let colorBrandPrimaryHover = UIColor(red: 0.145, green: 0.388, blue: 0.922, alpha: 1.0)
public static let colorNeutralWhite = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
public static let colorNeutralGray900 = UIColor(red: 0.059, green: 0.090, blue: 0.165, alpha: 1.0)
public static let fontSizeBase: CGFloat = 16.0
public static let fontSizeLg: CGFloat = 20.0
public static let spacingXs: CGFloat = 4.0
public static let spacingSm: CGFloat = 8.0
public static let spacingMd: CGFloat = 16.0
}Notice how Style Dictionary handled the transformations automatically:
- Naming conventions:
color.brand.primary→--color-brand-primary(CSS),color_brand_primary(Android),colorBrandPrimary(Swift) - Units:
16→16px(CSS),16sp(Android font),16dp(Android dimen),16.0(Swift CGFloat) - Color formats:
#3b82f6→ stays hex for CSS/Android, converts toUIColor(red:green:blue:)for iOS
This is the power of token-based systems: one source, automatic platform-appropriate outputs.
React Native Token Strategy
React Native projects have a unique advantage: JavaScript runs on both iOS and Android, allowing you to share token definitions directly without platform-specific transformations.
JavaScript Token Module
Create a tokens.js file that exports plain JavaScript objects:
// tokens.js - shared across React Native iOS and Android
export const colors = {
brand: {
primary: '#3b82f6',
primaryHover: '#2563eb',
},
neutral: {
white: '#ffffff',
gray900: '#0f172a',
}
};
export const fontSize = {
base: 16,
lg: 20,
xl: 24,
};
export const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
};
export const fontWeight = {
regular: '400',
semibold: '600',
bold: '700',
};Import these tokens in your React Native components:
// Button.tsx - React Native component using tokens
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
import { colors, fontSize, spacing, fontWeight } from './tokens';
export function Button({ children, onPress }) {
return (
<TouchableOpacity style={styles.button} onPress={onPress}>
<Text style={styles.text}>{children}</Text>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: colors.brand.primary,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: 8,
},
text: {
color: colors.neutral.white,
fontSize: fontSize.base,
fontWeight: fontWeight.semibold,
}
});Platform-Specific Overrides
When you need platform-specific values, use React Native's Platform.select():
// tokens.js with platform overrides
import { Platform } from 'react-native';
export const spacing = {
statusBarHeight: Platform.select({
ios: 44, // iOS status bar + safe area
android: 24, // Android status bar
}),
// ... other spacing tokens
};
export const fontFamily = {
regular: Platform.select({
ios: 'System',
android: 'Roboto',
}),
};Sharing Tokens with Web
If you're building a React Native app alongside a React web app, extract tokens into an npm package that both projects consume:
# Create shared token package
mkdir design-tokens
cd design-tokens
npm init -y
# tokens/index.js
export const colors = { /* ... */ };
export const spacing = { /* ... */ };
# Publish to npm or use in monorepo
npm publishBoth web and React Native projects import from the same package, ensuring perfect consistency across all React-based platforms.
Version Control and Token Updates
Managing token changes across platforms requires a systematic approach to prevent breaking changes and ensure smooth updates.
Semantic Versioning for Tokens
Treat your token package like any other dependency with semantic versioning:
- Patch (1.0.1): Bug fixes, color adjustments that don't affect design intent
- Minor (1.1.0): New tokens added, no breaking changes to existing tokens
- Major (2.0.0): Token renames, deletions, or value changes that require code updates
{
"name": "@yourcompany/design-tokens",
"version": "1.2.0",
"description": "Multi-platform design tokens for iOS, Android, and web"
}Automated CI/CD Pipeline
Set up continuous integration to build and publish tokens automatically when the source changes:
# .github/workflows/tokens.yml
name: Build Design Tokens
on:
push:
branches: [main]
paths: ['tokens.json']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm install
- run: npm run build:tokens # Runs Style Dictionary
- run: npm version patch
- run: npm publishWhen designers update tokens.json and push to main, the pipeline automatically:
- Builds web, Android, and iOS outputs
- Bumps the patch version
- Publishes to npm (or internal registry)
Platform teams then update their token dependency to get the latest values.
Breaking Change Detection
Implement automated checks to catch breaking changes before they reach production:
// detect-breaking-changes.js
const oldTokens = require('./tokens-v1.json');
const newTokens = require('./tokens.json');
function detectBreakingChanges(oldTokens, newTokens) {
const breaking = [];
// Check for deleted tokens
Object.keys(oldTokens).forEach(key => {
if (!newTokens[key]) {
breaking.push(`DELETED: ${key}`);
}
});
// Check for renamed tokens (value exists but key changed)
// Check for significant value changes (e.g., color shift > 10%)
return breaking;
}
const changes = detectBreakingChanges(oldTokens, newTokens);
if (changes.length > 0) {
console.error('Breaking changes detected:', changes);
process.exit(1); // Fail CI build
}Run this check in CI before publishing. If breaking changes are detected, require a major version bump and manual approval.
FAQ
Q: What format should design tokens use for multi-platform projects?
A: Use platform-agnostic JSON as your source format following the W3C Design Tokens specification. Store tokens as JSON objects with type and value properties, then transform them into platform-specific formats: CSS custom properties for web, XML resources for Android, and Swift constants for iOS using build tools like Style Dictionary.
Q: How do you keep design tokens synchronized across iOS, Android, and web?
A: Maintain a single source of truth in JSON format stored in a shared repository or npm package. Use Style Dictionary to build platform-specific outputs from this source during your CI/CD pipeline. When designers update the source tokens, automated builds generate new iOS, Android, and web token files that teams pull via dependency updates.
Q: What is the best approach for design tokens in React Native?
A: Create JavaScript token modules that export plain objects. React Native can import these directly on both iOS and Android, avoiding separate native token files. Use platform-specific overrides when needed with Platform.select() to handle differences like status bar height or system fonts while keeping most tokens shared.