Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Windows Tipps & SecurityGetting Repeated No Caller ID Calls? Here’s What’s Really Going On(22.09.2026 um 22:31 Uhr)
Windows Tipps & SecurityHöllenmaschine: Gaming-Peripherie für gut 1.800 Euro für die HMX 6(23.09.2026 um 10:20 Uhr)
Windows Tipps & SecurityDas nächste große Ding: KI-Agenten(23.09.2026 um 10:30 Uhr)
Sichere ProgrammierungHow AI Is Making Restaurant Menus Easier to Navigate(23.09.2026 um 10:55 Uhr)
Windows Tipps & SecurityGetting Repeated No Caller ID Calls? Here’s What’s Really Going On(22.09.2026 um 22:31 Uhr)
Windows Tipps & SecurityHöllenmaschine: Gaming-Peripherie für gut 1.800 Euro für die HMX 6(23.09.2026 um 10:20 Uhr)
Windows Tipps & SecurityDas nächste große Ding: KI-Agenten(23.09.2026 um 10:30 Uhr)
Sichere ProgrammierungHow AI Is Making Restaurant Menus Easier to Navigate(23.09.2026 um 10:55 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Build a multi-platform Design System, step by step

Let's build a multi-platform Design System, step by step Web, iOS, Android. One color definition. Changed once, propagated everywhere. Instead of a theory article, we're going to build a working POC, step by step. By the end, you'll…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!




Let's build a multi-platform Design System, step by step



Web, iOS, Android. One color definition. Changed once, propagated everywhere.



Instead of a theory article, we're going to build a working POC, step by step. By the end, you'll have a mini design system that generates CSS, Swift, and Kotlin from a single source, with two brands, light/dark themes, a demo, and regression tests. All the code is on GitHub — clone it and follow along.



The guiding thread: a design decision should exist in only one place, in a neutral form; everything else is derived from it automatically.






The problem we're solving



Your brand's primary color — a blue — lives in parallel in CSS variables (web), Swift (iOS), Kotlin (Android)… The problem isn't creating those values, it's keeping them in sync over time. One day someone changes the blue on web but not on Android, and the product becomes inconsistent. Multiply by 2 brands and 2 themes: the inconsistency becomes structural.



The root cause: the same decision is copied by hand into different formats. We're going to kill the copy.









Step 0 — The project






mkdir design-system-poc && cd design-system-poc
npm init -y
npm install -D style-dictionary typescript @types/node






Style Dictionary is the reference tool for turning "tokens → multiple targets". Add "type": "module" to your package.json (we write ESM) and a tsconfig.json in strict mode. We code in TypeScript: Node 24 runs it natively (type stripping), so node build.ts works with no compile step — tsc --noEmit is only there for type-checking.









Step 1 — Primitives (the ingredients)



A design token is a name → value pair describing a design intent, independent of any language. We start with the raw palette, with no business meaning.



tokens/primitives.json:




{
"color": {
"blue": { "300": { "value": "#60A5FA", "type": "color" },
"500": { "value": "#2563EB", "type": "color" },
"700": { "value": "#1D4ED8", "type": "color" } },
"green": { "500": { "value": "#059669", "type": "color" },
"700": { "value": "#047857", "type": "color" } },
"grey": { "0": { "value": "#FFFFFF", "type": "color" },
"100": { "value": "#F3F4F6", "type": "color" },
"900": { "value": "#111827", "type": "color" } }
},
"space": { "2": { "value": "8px", "type": "spacing" },
"4": { "value": "16px", "type": "spacing" } },
"radius": { "sm": { "value": "4px", "type": "borderRadius" },
"md": { "value": "8px", "type": "borderRadius" } }
}






These values "know" nothing about web or mobile. They're the neutral form everything else will derive from.









Step 2 — Semantic tokens (the secret to multi-brand)



The beginner trap: putting #2563EB directly in components. Instead, we create a semantic level that describes usage and points to a primitive (the {color.blue.500} syntax is a Style Dictionary reference).



tokens/semantic.brandA.light.json:




{
"color": {
"primary": { "value": "{color.blue.500}", "type": "color" },
"primary-hover": { "value": "{color.blue.700}", "type": "color" },
"on-primary": { "value": "{color.grey.0}", "type": "color" },
"surface": { "value": "{color.grey.0}", "type": "color" },
"on-surface": { "value": "{color.grey.900}", "type": "color" }
}
}






The dark theme keeps the same semantic names, but rewires to different primitives — tokens/semantic.brandA.dark.json:




{
"color": {
"primary": { "value": "{color.blue.300}", "type": "color" },
"surface": { "value": "{color.grey.900}", "type": "color" },
"on-surface": { "value": "{color.grey.100}", "type": "color" }
}
}






And a 2nd brand = just different wiring — tokens/semantic.brandB.light.json:




{
"color": {
"primary": { "value": "{color.green.500}", "type": "color" },
"primary-hover": { "value": "{color.green.700}", "type": "color" },
"surface": { "value": "{color.grey.0}", "type": "color" }
}
}






Why this matters: your components reference only color-primary, never blue-500. Switching brand or theme = rewiring the semantic links, without touching a single line of any component.









Step 3 — Generate for each platform



We give Style Dictionary the tokens + the target formats, and it generates the rest. The key point: ship idiomatic code per platform, not raw JSON. An iOS dev wants typed Swift.



We have 3 combinations (brand × theme) to generate, so we loop. build.ts:




import StyleDictionary from 'style-dictionary';
import type { Config } from 'style-dictionary/types';

const combinations = [
{ brand: 'brandA', theme: 'light' },
{ brand: 'brandA', theme: 'dark' },
{ brand: 'brandB', theme: 'light' },
];

const platformsFor = (name: string): Config['platforms'] => ({
css: { transformGroup: 'css', buildPath: 'build/web/',
files: [{ destination: `${name}.css`, format: 'css/variables' }] },
ios: { transformGroup: 'ios-swift', buildPath: 'build/ios/',
files: [{ destination: `${name}.swift`, format: 'ios-swift/class.swift',
options: { className: 'Tokens' } }] },
android: { transformGroup: 'compose', buildPath: 'build/android/',
files: [{ destination: `${name}.kt`, format: 'compose/object',
options: { className: 'Tokens', packageName: 'com.monorg.tokens' } }] },
json: { transformGroup: 'js', buildPath: 'build/json/',
files: [{ destination: `${name}.json`, format: 'json/flat' }] },
});

for (const { brand, theme } of combinations) {
const name = `${brand}-${theme}`;
const sd = new StyleDictionary({
// Source = the common layer (primitives) + this combination's semantic links.
source: ['tokens/primitives.json', `tokens/semantic.${brand}.${theme}.json`],
platforms: platformsFor(name),
});
await sd.buildAllPlatforms();
}









node build.ts






And there's the same decision, translated into each language. Web (build/web/brandA-light.css):




:root {
--color-primary: #2563eb;
--color-on-primary: #ffffff;
--color-surface: #ffffff;
--color-on-surface: #111827;
--space-4: 16px;
--radius-md: 8px;
}






iOS (build/ios/brandA-light.swift):




public class Tokens {
public static let colorPrimary = UIColor(red: 0.145, green: 0.388, blue: 0.922, alpha: 1)
// ...
}






Android / Compose (build/android/brandB-light.kt):




package com.monorg.tokens
object Tokens {
val colorPrimary = Color(0xff059669) // brandB → green
val colorPrimaryHover = Color(0xff047857)
}






One source, three languages, idiomatic.









Step 4 — A component that consumes (and the magic of the switch)



The component uses only semantic tokens, no hard-coded colors. demo/index.html:




<link id="theme" rel="stylesheet" href="../build/web/brandA-light.css" />
<style>
.btn {
background: var(--color-primary);
color: var(--color-on-primary);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
}
.btn:hover { background: var(--color-primary-hover); }
</style>

<button class="btn">Primary action</button>

<script>
// The only switch: we swap the token stylesheet. The component does not change.
document.getElementById('theme').href = '../build/web/brandA-dark.css';
</script>






Change brandA-lightbrandA-darkbrandB-light: the button re-themes itself. Not a single line of the component moves. That's the whole point of the semantic level.









Step 5 — Test regressions (the indispensable safety net)



In a system consumed everywhere, a silent regression spreads everywhere at once. We add a net, with zero dependencies, using node:test.



A deliberate choice: writing these tests in Gherkin (Feature / Scenario / Given / When / Then). Why? Because a design system is consumed as much by non-developers (designers, PMs) as by devs. Phrased this way, the tests become documentation that reads like sentences: "Given brand A in dark, when generated, then primary points to blue 300". No need to read code to know what the system guarantees. test/tokens.test.ts:




import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';

const read = (combo: string): Record<string, string> =>
JSON.parse(readFileSync(new URL(`../build/json/${combo}.json`, import.meta.url), 'utf8'));

describe('Feature: semantic links resolve to the right primitives', () => {
describe('Scenario: the dark theme reuses the same semantic names', () => {
it('Given brand A in dark, When generated, Then primary points to blue 300', () => {
const tokens = read('brandA-dark');
assert.equal(tokens.ColorPrimary, '#60a5fa');
assert.equal(tokens.ColorSurface, '#111827');
});
});

describe('Scenario: switching brand is just re-wiring the links', () => {
it('Given brand B in light, When generated, Then primary = green', () => {
assert.equal(read('brandB-light').ColorPrimary, '#059669');
});
});
});









node build.ts && node --test "test/**/*.test.ts"
# ✔ Feature: semantic links resolve to the right primitives
# ✔ Scenario: the dark theme reuses the same semantic names
# ✔ Scenario: switching brand is just re-wiring the links






If someone breaks a primitive or a link, the test fails before the regression reaches the products. The repo wires this into a GitHub Actions CI that, on every push, builds all combinations, checks the multi-platform artifacts were actually generated, and runs the tests — so a broken link never reaches main. In production, you complement this with snapshot tests on the generated files (any change to an output becomes explicit in review) and, on the component side, a Storybook + automated visual review (Chromatic-style).









Step 6 — Assets (the real challenge: icons & fonts)



Colors are the easy part. Where a design system earns its stripes is on assets: an icon or a font must also start from a single source and be translated into each platform's format.



The most telling case: Android doesn't read SVG natively, it needs a Vector Drawable XML. So we write a small SVG → Vector Drawable converter (excerpt):




export function svgToVectorDrawable(svg: string, { size = 24 } = {}): string {
const [, , w, h] = svg.match(/viewBox="([\d.\s-]+)"/)![1].trim().split(/\s+/).map(Number);
const paths = [...svg.matchAll(/<path\b[^>]*\bd="([^"]+)"[^>]*>/gi)].map((m) => {
const fill = (m[0].match(/fill="([^"]+)"/) || [])[1] ?? 'currentColor';
const color = fill === 'currentColor' ? '#FF000000' : `#FF${fill.slice(1).toUpperCase()}`;
return ` <path android:fillColor="${color}" android:pathData="${m[1]}" />`;
});
return `<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="
${size}dp" android:height="${size}dp"
android:viewportWidth="
${w}" android:viewportHeight="${h}">
${paths.join('\n')}
</vector>`
;
}






From a single assets/icons/check.svg, the pipeline produces:





  • Web → the SVG as-is + a JSON manifest;


  • iOS → a type-safe enum Icon (no more "magic strings") + the SVG;


  • Androidic_check.xml (Vector Drawable) + an enum Icon.



Same logic for the font: one Inter.ttf@font-face (web), FontRegistrar.swift (iOS), res/font/ (Android). The full code is in the repo (src/buildAssets.ts).



That's the difference between "a shared color file" and a real design system: the multi-platform asset chain.









Step 7 — Distribute it like a real product



A design system only has value if it's easy to consume. Never ask people to copy files: publish to native package managers, and version with SemVer (with alpha / beta / rc / stable channels).




npm install @monorg/[email protected]      # web









implementation("monorg:design-tokens:1.2.0") // android (Maven)






Automate it via CI: a version tag triggers a pipeline that builds all formats and publishes each package to its registry (npm / SPM / Maven). No manual step = no drift between the repo and what's shipped.




# pseudo-pipeline
on: tag 'tokens-v*'
jobs:
build: node build.ts
publish: [ npm, spm, maven ] # one job per registry









The real win: each consumer pins ITS version



This is the point people underestimate at first. Once tokens are published as versioned packages, each application picks the version it consumes — and that's what makes the system usable at scale:





  • Total decoupling. The design team can publish 1.3.0 today without breaking anything: the iOS app stays on 1.2.0 until it decides to bump. Nobody is forced to move at the same time.


  • Progressive rollout. You test a new palette on one product (which goes to 1.3.0-alpha.1) while the others stay stable. If it breaks, the blast radius is one repo, not all of them.


  • Reproducible builds. A lockfile pinned to 1.2.0 produces exactly the same rendering in six months. A "shared folder" that changes under your feet does not.


  • History and rollback. A visual regression in prod? Revert to the previous version in one line (@1.1.0) while you fix it. No panicked hotfix across 6 codebases.




design-tokens   v1.3.0  ← latest published
├── web app "@monorg/design-tokens": "1.3.0" (up to date)
├── iOS app from: "1.2.0" (bumps when it wants)
└── Android app "monorg:design-tokens:1.2.0" (same)






The design system becomes a dependency like any other: you update it when you're ready, you read the CHANGELOG, you pin, you roll back. It's exactly this versioning mechanism that turns a "shared folder" into infrastructure.









Wrapping up



A design system is not a button library. It's infrastructure, and its value is in the chain:





  1. A single source of truth: the tokens, in neutral form.


  2. Three levels: primitives → semantic → component. That's what makes multi-brand manageable.


  3. A generation to idiomatic code per platform (Style Dictionary).


  4. A distribution versioned with SemVer, via native package managers.


  5. Tests (Gherkin + snapshots + visual review) to change without fear — and that document the system, even for non-devs.



The full POC code is here → github.com/ibrahimdans/design-system-poc. Clone it, npm install, npm run demo.



The real question isn't "which components should go in my design system?". It's:



"Do my design decisions have a single source of truth, or are they copied by hand?"



Start there.






Unifying several products or several platforms? I design design systems that scale. Let's talk on LinkedIn.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Build a multi-platform Design System, step by step

Thematisch verwandte Begriffe: Build, multiplatform, Design, System · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-96258 | A vulnerability has been found in onSite internet GmbH Auktion NG Auktio…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick