UI/UX Atlas
Design Systems Advanced

Multi-Platform Token Output

Shipping design tokens once and consuming them correctly across web, iOS, Android, and beyond — without divergence, duplication, or platform-specific hacks.

8 min read

The full lesson

Design tokens only deliver on their promise when every platform speaks the same visual language. Define a token once in a single JSON file, and it should appear as a CSS custom property on the web, a Swift Color constant on iOS, a Kotlin object on Android, and a Flutter ThemeData entry — all generated from one source of truth.

Without a deliberate output strategy, teams end up maintaining separate hardcoded style files for each platform. Those files drift the moment a designer changes a hex value.

This lesson covers the architecture, tooling, and key decisions behind a solid multi-platform token pipeline — from authoring a DTCG-compliant token set to platform-specific transforms and automated delivery.

Why Platform Output Is the Hard Part

Writing tokens is the easy part. Delivering them correctly to five different consumers is where most systems break. Each platform differs in four key ways:

  • Value syntax — CSS uses oklch(60% 0.15 250). Swift uses UIColor(red:green:blue:alpha:) or a named asset. Android uses @color/token_name in XML or Color(0xFF...) in Jetpack Compose.
  • File format.css and .scss for web; .xcassets for iOS color assets; colors.xml or Theme.kt for Android; .dart files for Flutter.
  • Reference model — CSS custom properties resolve at runtime. Swift and Kotlin constants resolve at compile time. This difference changes how token aliases behave on each platform.
  • Theming mechanism — CSS uses the cascade and custom properties. iOS uses UITraitCollection. Android uses themes.xml. Your token pipeline must produce outputs that plug into each of these natively.

The old approach was manual: a design systems engineer would maintain a colors.css, a colors.swift, and a colors.xml file and try to keep them in sync through discipline and code review. In practice, they drifted within weeks.

A modern pipeline treats platform output files as build artifacts — they are generated, not authored.

The W3C DTCG Token Format as the Single Source

The W3C Design Token Community Group (DTCG) spec defines a stable JSON structure. Every token has a $value and a $type, and groups can nest hierarchically. As of 2024–2025, this format has broad tooling support and is the right choice for any system starting fresh.

A minimal DTCG-compliant token file looks like this:

{
  "color": {
    "brand": {
      "primary": {
        "$value": "oklch(55% 0.2 264)",
        "$type": "color"
      },
      "primary-subtle": {
        "$value": "{color.brand.primary}",
        "$type": "color"
      }
    },
    "surface": {
      "default": {
        "$value": "{color.neutral.0}",
        "$type": "color"
      }
    }
  },
  "spacing": {
    "4": {
      "$value": "16",
      "$type": "dimension"
    }
  }
}

Key rules for DTCG-compliant authoring:

  • Use $value and $type — the $-prefixed keys are the spec standard. Older tooling used bare value and type without the prefix. Migrate away from the older style.
  • Aliases use curly-brace dot-notation inside $value strings (as shown in the code sample above).
  • Use OKLCH for colors. OKLCH is perceptually uniform, gamut-aware, and generates predictable tonal scales algorithmically. HSL and raw hex are outdated authoring choices.
  • Store dimensions as unitless numbers with a dimension type. The transform pipeline adds the correct unit for each platform.

Style Dictionary: Transform Architecture

Style Dictionary (by Amazon, actively maintained, v4 as of 2024) is the dominant open-source tool for multi-platform token output. Its core model has four steps:

  1. Load — read one or more JSON/DTCG token files.
  2. Transform — apply per-platform value transforms (for example, convert oklch() to rgba() for platforms that need legacy color formats, or convert logical units for iOS).
  3. Format — serialize the transformed tokens into the target file format.
  4. Output — write to the configured output directories.

A minimal Style Dictionary v4 config for web, iOS, and Android:

// style-dictionary.config.mjs
export default {
  source: ["tokens/**/*.json"],
  platforms: {
    css: {
      transformGroup: "css",
      prefix: "ds",
      buildPath: "build/web/",
      files: [
        {
          destination: "tokens.css",
          format: "css/variables",
          options: { outputReferences: true }
        }
      ]
    },
    ios_swift: {
      transformGroup: "ios-swift",
      buildPath: "build/ios/",
      files: [
        {
          destination: "DesignTokens.swift",
          format: "ios-swift/class.swift",
          className: "DesignTokens"
        }
      ]
    },
    android: {
      transformGroup: "android",
      buildPath: "build/android/src/main/res/values/",
      files: [
        {
          destination: "tokens_colors.xml",
          format: "android/colors",
          filter: { attributes: { category: "color" } }
        }
      ]
    }
  }
};

The outputReferences: true option on the CSS platform preserves alias chains — for example, var(--ds-color-surface-default) points at var(--ds-color-neutral-0) instead of resolving to a flat value. This enables runtime theming.

Compiled platforms like iOS and Android have no runtime cascade, so they resolve aliases at build time.

Platform-Specific Transform Concerns

Web (CSS Custom Properties)

  • Use outputReferences: true so semantic tokens compose at runtime in the browser. Dark mode can then redefine primitive values, and all semantic tokens pick them up automatically.
  • Scope tokens to :root by default. Scope theme overrides to a [data-theme="dark"] attribute selector.
  • OKLCH is natively supported in all modern browsers. For legacy targets, run a PostCSS plugin to generate @supports fallbacks with rgba() equivalents.

iOS (Swift / SwiftUI)

  • Style Dictionary’s ios-swift transform group outputs dimension tokens as CGFloat values. Verify that your design token scale uses points-equivalent logical units to avoid off-by-one scaling errors.
  • Color tokens can output as Color (SwiftUI) or UIColor (UIKit). For dark mode, generate an .xcassets color set with light and dark appearance variants, or produce a Swift extension that reads from UITraitCollection.
  • Because Swift has no runtime cascade, aliases resolve to their final values at build time. Semantic tokens lose the “live pointer” benefit that CSS custom properties provide.

Android (XML / Compose)

  • For legacy View-based Android: output colors.xml and dimens.xml using Style Dictionary’s built-in android transform group.
  • For Jetpack Compose: output a Kotlin file with a MaterialTheme extension. Style Dictionary has community-maintained Compose formatters. Terrazzo ships first-class Compose output as an alternative.
  • Android uses ARGB hex ordering (0xFFRRGGBB), not RGBA. The transform group handles this automatically, but verify the output when OKLCH requires an intermediate sRGB resolve step.

Theming Across Platforms

Dark mode and brand theming require different implementation strategies per platform. Your token structure should make them composable everywhere.

The Three-File Pattern

A practical structure for multi-theme, multi-platform output:

FileContentsUsed by
primitives.jsonRaw color palette (neutral-0 through neutral-1000), spacing scale, radiiBuild tool only
semantic.light.jsonSurface, text, border tokens referencing primitives — light valuesAll platforms, light theme
semantic.dark.jsonSame token names, different primitive references — dark valuesAll platforms, dark theme

This pattern keeps token names identical across themes. On web, the dark file generates a [data-theme="dark"] block that overrides the same custom property names. On iOS, it generates the dark-appearance entries in .xcassets. On Android, it populates a night/ resource qualifier folder.

Avoid Token Naming That Encodes Mode

Do not name semantic tokens light-surface-default or dark-text-primary. Token names should describe purpose, not appearance mode. The theme file is what determines which value applies in which context.

Do

Name tokens by role: color-surface-default, color-text-primary, color-border-subtle. Output two separate files — one per theme — that define the same names with different values. Each platform’s theming mechanism selects the right file.

Don't

Encode the theme into the token name: light-background, dark-text-inverse, blue-500. These names break the moment you add a third theme or rebrand, and they give platforms no clean way to swap values via their native theming systems.

Automating Delivery: CI Pipeline Integration

A token pipeline that only runs when someone triggers it manually will drift. The right model is continuous delivery: every merge to the token repository triggers a build that pushes generated outputs to all consumers.

A typical CI pipeline:

# .github/workflows/build-tokens.yml
name: Build and distribute tokens
on:
  push:
    branches: [main]
    paths: ["tokens/**"]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build:tokens
      # Outputs land in build/web/, build/ios/, build/android/

      - name: Publish web tokens to npm
        run: npm publish --workspace packages/tokens-web

      - name: Open PR in iOS repo with updated Swift file
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.CROSS_REPO_PAT }}
          path: build/ios/
          branch: tokens/auto-update

      - name: Open PR in Android repo with updated XML
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.CROSS_REPO_PAT }}
          path: build/android/
          branch: tokens/auto-update

Three key decisions in this setup:

  • Web tokens publish to npm — platform teams install the package and get updates through their normal dependency workflow.
  • Native platform outputs open PRs — instead of force-pushing to native repos, the pipeline opens a pull request. This gives native engineers visibility and a merge gate, rather than silent file mutations.
  • The paths filter prevents CI from running on every commit — only token file changes trigger a build.

Tooling Comparison

ToolDTCG SupportPlatformsNotes
Style Dictionary v4NativeWeb, iOS, Android, Flutter, customMost extensible; large ecosystem; requires config work
TerrazzoNativeWeb, iOS, Android, ComposeOpinionated; faster setup; first-class Compose
Token Pipeline (Figma Tokens plugin)PartialWeb-focusedGood for Figma-first workflows; less platform depth
Theo (deprecated)NoWeb onlyAvoid for new projects

Style Dictionary is the most flexible choice for teams with non-standard output needs. Terrazzo trades configurability for a faster setup and is a strong choice for greenfield multi-platform projects.

Common Failure Modes

Teams implementing multi-platform token output tend to hit the same problems:

  • Alias resolution depth — aliases that reference other aliases (three or more hops) can cause circular reference errors in some tooling. Keep alias chains shallow: primitives referenced by semantics, semantics referenced by components. No component tokens referencing other component tokens.
  • Dimension unit mismatch — a token stored as 16 (unitless dimension) is correct. A token stored as "16px" will produce "16pxpt" when the iOS transform appends pt. Audit your dimension tokens before adding native platform outputs.
  • Name collisions in generated code — Swift and Kotlin have reserved words and naming constraints. A token named default conflicts with a Swift reserved word. Add a prefix (ds- or token-) at the config level to avoid this.
  • Multiple source-of-truth syndrome — if the Figma file is the “real” token source and the JSON file is a manual export, they will drift. Use the Figma Tokens Plugin or Token Studio with a GitHub sync to make the JSON the canonical source that Figma reads from, not the other way around.