<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zed Alleys</title>
    <description>The latest articles on DEV Community by Zed Alleys (@zedalleys).</description>
    <link>https://dev.to/zedalleys</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4165741%2F970b83f6-827e-4b08-a3c3-042f0c1aa3a1.png</url>
      <title>DEV Community: Zed Alleys</title>
      <link>https://dev.to/zedalleys</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zedalleys"/>
    <language>en</language>
    <item>
      <title>Your cross-platform design system may be lying about parity</title>
      <dc:creator>Zed Alleys</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:21:03 +0000</pubDate>
      <link>https://dev.to/zedalleys/your-cross-platform-design-system-may-be-lying-about-parity-4bj0</link>
      <guid>https://dev.to/zedalleys/your-cross-platform-design-system-may-be-lying-about-parity-4bj0</guid>
      <description>&lt;p&gt;Mine was.&lt;/p&gt;

&lt;p&gt;Not deliberately — nobody sat down to write something false.&lt;/p&gt;

&lt;p&gt;But the documentation said a thing that was &lt;strong&gt;not true&lt;/strong&gt;, and nothing in the repository could tell.&lt;/p&gt;

&lt;p&gt;We maintain component implementations for &lt;strong&gt;React, SwiftUI, Jetpack Compose, and Flutter&lt;/strong&gt; — the four catalogue-complete platforms this story is about.&lt;/p&gt;

&lt;p&gt;Every component page listed its supported platforms.&lt;/p&gt;

&lt;p&gt;Every listing was a string in a JSON file that a human typed.&lt;/p&gt;

&lt;p&gt;And nothing checked whether the implementation it claimed actually existed.&lt;/p&gt;

&lt;p&gt;Then I wrote a script that checked.&lt;/p&gt;

&lt;p&gt;On its first run it reported that &lt;code&gt;direction-provider&lt;/code&gt; — advertised on SwiftUI, Compose, and Flutter — had &lt;strong&gt;no implementation on any of them&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not a partial port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No file.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Parity is a claim, and claims decay
&lt;/h2&gt;

&lt;p&gt;A single-platform library has a useful property: the documentation and the code often live in the same repository, in the same language, and sometimes come from the same source.&lt;/p&gt;

&lt;p&gt;Delete a component and something tends to break.&lt;/p&gt;

&lt;p&gt;A cross-platform design system loses that property almost immediately.&lt;/p&gt;

&lt;p&gt;The React implementation is TypeScript.&lt;/p&gt;

&lt;p&gt;The iOS implementation is Swift.&lt;/p&gt;

&lt;p&gt;The Android implementation is Kotlin.&lt;/p&gt;

&lt;p&gt;Flutter has Dart.&lt;/p&gt;

&lt;p&gt;They are compiled by different toolchains, on different machines, in jobs the web build may know nothing about.&lt;/p&gt;

&lt;p&gt;The matrix tying them together is not necessarily code.&lt;/p&gt;

&lt;p&gt;It is often just &lt;strong&gt;a description of code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And that description tends to decay in one direction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Toward optimism.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody forgets to add a platform they just shipped.&lt;/p&gt;

&lt;p&gt;People forget to remove one they abandoned.&lt;/p&gt;

&lt;p&gt;Or one they planned.&lt;/p&gt;

&lt;p&gt;Or one they never actually started.&lt;/p&gt;

&lt;p&gt;The gap widens silently, usually in the flattering direction, until somebody tries to use what the documentation said existed.&lt;/p&gt;

&lt;p&gt;So the useful question about any cross-platform design system — including this one — is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How many components does it have?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What would fail if this claim were false?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is &lt;strong&gt;“nothing”&lt;/strong&gt;, the claim is decoration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three levels of verification
&lt;/h2&gt;

&lt;p&gt;I think of platform verification in three levels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 0 — the name is spelled correctly
&lt;/h3&gt;

&lt;p&gt;Our original check did this.&lt;/p&gt;

&lt;p&gt;It validated that &lt;code&gt;"SwiftUI"&lt;/code&gt; was a known platform string.&lt;/p&gt;

&lt;p&gt;That is useful, but it cannot tell you whether any SwiftUI implementation actually exists.&lt;/p&gt;

&lt;p&gt;A system can pass Level 0 perfectly while documenting components that were never built.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 1 — source exists
&lt;/h3&gt;

&lt;p&gt;Look for a real implementation file in the platform package.&lt;/p&gt;

&lt;p&gt;A component claims SwiftUI?&lt;/p&gt;

&lt;p&gt;There should be Swift source.&lt;/p&gt;

&lt;p&gt;Claims Compose?&lt;/p&gt;

&lt;p&gt;There should be Kotlin source.&lt;/p&gt;

&lt;p&gt;Claims Flutter?&lt;/p&gt;

&lt;p&gt;There should be Dart source.&lt;/p&gt;

&lt;p&gt;This check is cheap, deterministic, and scriptable from almost anywhere.&lt;/p&gt;

&lt;p&gt;And it catches an important class of failure:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“We said we built this, but there is no implementation.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Level 2 — it compiles
&lt;/h3&gt;

&lt;p&gt;A file existing still does not prove that its API is correct.&lt;/p&gt;

&lt;p&gt;The platform's own compiler needs to resolve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the symbol,&lt;/li&gt;
&lt;li&gt;the arguments,&lt;/li&gt;
&lt;li&gt;the parameter names,&lt;/li&gt;
&lt;li&gt;the types,&lt;/li&gt;
&lt;li&gt;and the surrounding usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the level that catches a &lt;strong&gt;wrong API&lt;/strong&gt;, rather than just a missing one.&lt;/p&gt;

&lt;p&gt;Most projects that verify anything can accidentally stop around Level 0.&lt;/p&gt;

&lt;p&gt;The dangerous part is that Level 0 still &lt;em&gt;feels&lt;/em&gt; like verification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it has a script,&lt;/li&gt;
&lt;li&gt;it runs in CI,&lt;/li&gt;
&lt;li&gt;and it goes green.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Level 1 caught
&lt;/h2&gt;

&lt;p&gt;The first useful failure was &lt;code&gt;direction-provider&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It claimed three native platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SwiftUI,&lt;/li&gt;
&lt;li&gt;Jetpack Compose,&lt;/li&gt;
&lt;li&gt;Flutter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There was no implementation on any of them.&lt;/p&gt;

&lt;p&gt;The reason turned out to be more interesting than the mismatch.&lt;/p&gt;

&lt;p&gt;Those frameworks already carry layout direction natively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SwiftUI → &lt;code&gt;environment(\.layoutDirection)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compose → &lt;code&gt;LocalLayoutDirection&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Flutter → &lt;code&gt;Directionality&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There was never really anything useful to port.&lt;/p&gt;

&lt;p&gt;So the correct documentation was not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Supported on every platform.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“React-specific by design; the native frameworks already provide the capability.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Another example was &lt;code&gt;combobox&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It appeared to be a React component.&lt;/p&gt;

&lt;p&gt;But there was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no &lt;code&gt;combobox.tsx&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;no &lt;code&gt;Combobox&lt;/code&gt; export,&lt;/li&gt;
&lt;li&gt;and no standalone implementation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is a recipe built from &lt;code&gt;Command&lt;/code&gt; primitives.&lt;/p&gt;

&lt;p&gt;That is still useful.&lt;/p&gt;

&lt;p&gt;It is simply &lt;strong&gt;not a component&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It says so now.&lt;/p&gt;

&lt;p&gt;Correcting those claims moved the published numbers down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SwiftUI: &lt;strong&gt;91 → 90&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Compose: &lt;strong&gt;90 → 89&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Flutter: &lt;strong&gt;91 → 90&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Components on all four catalogue-complete platforms: &lt;strong&gt;90 → 89&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The numbers got worse.&lt;/p&gt;

&lt;p&gt;The documentation got true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same event.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The check should insult you
&lt;/h2&gt;

&lt;p&gt;A verifier that only confirms what you already believe is not much of a verifier.&lt;/p&gt;

&lt;p&gt;On its first run, ours also flagged &lt;code&gt;sonner&lt;/code&gt; as missing on SwiftUI and Flutter.&lt;/p&gt;

&lt;p&gt;That result was wrong.&lt;/p&gt;

&lt;p&gt;The implementations existed, but under platform-native names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Toaster.swift&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toaster.dart&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part was how to fix the checker.&lt;/p&gt;

&lt;p&gt;The tempting solution is to add a lookup table inside the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sonner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SwiftUI&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Toaster&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But now the checker knows something the source of truth does not.&lt;/p&gt;

&lt;p&gt;That is a trap.&lt;/p&gt;

&lt;p&gt;Instead, the alias belongs beside the claim in the manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"sonner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platforms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"React"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SwiftUI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Compose"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Flutter"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceNames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SwiftUI"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Toaster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Flutter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"toaster"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When the checker is wrong, fix the data — not the checker.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A checker full of exceptions eventually stops being a specification and becomes another undocumented source of truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Level 2 caught
&lt;/h2&gt;

&lt;p&gt;Source existing is not the same thing as source being correct.&lt;/p&gt;

&lt;p&gt;The gap becomes especially dangerous in documentation, because examples are easy to write by hand and surprisingly easy to never compile.&lt;/p&gt;

&lt;p&gt;Our component pages carried roughly &lt;strong&gt;300 hand-written native snippets&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I checked every &lt;code&gt;Kinetix*&lt;/code&gt; identifier in them against the real packages.&lt;/p&gt;

&lt;p&gt;The first symbol-level pass looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SwiftUI: &lt;strong&gt;164 symbols&lt;/strong&gt;, all real&lt;/li&gt;
&lt;li&gt;Flutter: &lt;strong&gt;175 symbols&lt;/strong&gt;, all real&lt;/li&gt;
&lt;li&gt;Compose: &lt;strong&gt;182 symbols&lt;/strong&gt;, with &lt;strong&gt;three that did not exist&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worst example was on the Chart page.&lt;/p&gt;

&lt;p&gt;The documentation showed this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// KinetixChart is a hand-drawn CustomPaint bar chart over the&lt;/span&gt;
&lt;span class="c1"&gt;// --chart-1…5 palette (no charting dependency). Line/area are a follow-up.&lt;/span&gt;
&lt;span class="nc"&gt;KinetixChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;KinetixChartPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Jan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;186f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seriesIndex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither &lt;code&gt;KinetixChart&lt;/code&gt; nor &lt;code&gt;KinetixChartPoint&lt;/code&gt; existed in the Compose package at the time.&lt;/p&gt;

&lt;p&gt;There was no Compose Chart implementation.&lt;/p&gt;

&lt;p&gt;The manifest already said so correctly.&lt;/p&gt;

&lt;p&gt;And there was another clue hiding in plain sight:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CustomPaint&lt;/code&gt; is a &lt;strong&gt;Flutter API&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Flutter implementation's rationale had effectively been pasted onto an invented Compose example.&lt;/p&gt;

&lt;p&gt;The website rendered that code under a tab labelled &lt;strong&gt;Android&lt;/strong&gt;, beside real examples, with nothing visually distinguishing it from working API.&lt;/p&gt;

&lt;p&gt;Nobody deliberately lied.&lt;/p&gt;

&lt;p&gt;Someone wrote a plausible snippet for a component they expected to build.&lt;/p&gt;

&lt;p&gt;And there was &lt;strong&gt;no compiler between that sentence and the reader&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A Compose chart has since been written.&lt;/p&gt;

&lt;p&gt;Interestingly, the API that eventually landed is close enough to the invented one to be uncomfortable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KinetixChart&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KinetixChartPoint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;label&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;seriesIndex&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the old example still would not compile against the actual implementation.&lt;/p&gt;

&lt;p&gt;The real signature requires a &lt;code&gt;description&lt;/code&gt;, because a chart that a screen reader cannot narrate is not finished.&lt;/p&gt;

&lt;p&gt;Guessing a future API correctly is not the same as having that API today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where symbol checking stops
&lt;/h2&gt;

&lt;p&gt;Symbol checking is cheap.&lt;/p&gt;

&lt;p&gt;It catches a lot.&lt;/p&gt;

&lt;p&gt;But it is still not compilation.&lt;/p&gt;

&lt;p&gt;Consider this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;KinetixDataTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;KinetixDataColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sortKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A symbol check can catch one problem immediately:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;KinetixDataColumn&lt;/code&gt; does not exist.&lt;/p&gt;

&lt;p&gt;The real class is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;KinetixColumn&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But two other problems are invisible to a simple symbol scan.&lt;/p&gt;

&lt;p&gt;The parameter is &lt;code&gt;data&lt;/code&gt;, not &lt;code&gt;rows&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And the trailing lambda binds to the constructor's last parameter, &lt;code&gt;weight: Float&lt;/code&gt;, rather than a &lt;code&gt;cell&lt;/code&gt; renderer.&lt;/p&gt;

&lt;p&gt;Every symbol could exist and the snippet could still fail to compile.&lt;/p&gt;

&lt;p&gt;That is why Level 2 matters.&lt;/p&gt;

&lt;p&gt;The direction we are taking is to move examples into files the real platform CI builds, then quote those exact regions on the website.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// kx-usage:button-demo&lt;/span&gt;
&lt;span class="nc"&gt;KinetixButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Button"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// kx-usage:end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler sees the example.&lt;/p&gt;

&lt;p&gt;The website reads the same example.&lt;/p&gt;

&lt;p&gt;And a drift check fails if the two diverge.&lt;/p&gt;

&lt;p&gt;One source.&lt;/p&gt;

&lt;p&gt;Two surfaces.&lt;/p&gt;

&lt;p&gt;One failure path.&lt;/p&gt;

&lt;p&gt;Six of our hundred-odd demo examples currently take their native source that way.&lt;/p&gt;

&lt;p&gt;That number is intentionally specific.&lt;/p&gt;

&lt;p&gt;Saying &lt;strong&gt;“six”&lt;/strong&gt; is more useful than saying &lt;strong&gt;“we're migrating.”&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The same failure, one layer up
&lt;/h2&gt;

&lt;p&gt;Our Blocks page had the same problem at a larger scale.&lt;/p&gt;

&lt;p&gt;Blocks are composed examples — things like sign-in cards and application layouts.&lt;/p&gt;

&lt;p&gt;Originally, one large file contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the live preview,&lt;/li&gt;
&lt;li&gt;a hand-written React snippet,&lt;/li&gt;
&lt;li&gt;a hand-written Compose snippet,&lt;/li&gt;
&lt;li&gt;a hand-written Flutter snippet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They were supposed to represent the same thing.&lt;/p&gt;

&lt;p&gt;They were not generated from the same source.&lt;/p&gt;

&lt;p&gt;And they had already drifted.&lt;/p&gt;

&lt;p&gt;For example, the React snippet showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;id="email"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the live component rendered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;id="bl-email"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two copies of one thing.&lt;/p&gt;

&lt;p&gt;No verification.&lt;/p&gt;

&lt;p&gt;Already different.&lt;/p&gt;

&lt;p&gt;Then there was the CTA banner.&lt;/p&gt;

&lt;p&gt;Its own copy said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“React, SwiftUI, Compose and Flutter from a single source.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But it had &lt;strong&gt;no SwiftUI source&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And the three snippets that did exist were independently typed strings.&lt;/p&gt;

&lt;p&gt;The sentence was wrong twice in eleven words — inside the example intended to demonstrate the claim.&lt;/p&gt;

&lt;p&gt;Today, every block snippet comes from a file that compiles.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five questions worth asking
&lt;/h2&gt;

&lt;p&gt;Whether you are evaluating a design system or maintaining one, these are the questions I would ask.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Where does the platform matrix live, and what reads it?
&lt;/h3&gt;

&lt;p&gt;A hand-maintained table in a README is a description.&lt;/p&gt;

&lt;p&gt;It is not automatically a specification.&lt;/p&gt;

&lt;p&gt;Ask what consumes that matrix and what verifies it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. What happens in CI if a component is listed for a platform with no source?
&lt;/h3&gt;

&lt;p&gt;If the answer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Nothing.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;then the listing does not carry much weight.&lt;/p&gt;

&lt;p&gt;A platform claim should have a failure condition.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Are the documentation snippets compiled?
&lt;/h3&gt;

&lt;p&gt;If not, assume some fraction of them are fiction.&lt;/p&gt;

&lt;p&gt;In our case, the obvious symbol-level error rate was around &lt;strong&gt;1%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That sounds small until the 1% is an API you publish as real.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Does the system distinguish “not ported” from “ported”?
&lt;/h3&gt;

&lt;p&gt;Sometimes &lt;em&gt;not porting something&lt;/em&gt; is the correct architecture.&lt;/p&gt;

&lt;p&gt;That should be documented explicitly.&lt;/p&gt;

&lt;p&gt;For components deliberately not implemented on a platform, we now explain the native composition or framework capability to use instead.&lt;/p&gt;

&lt;p&gt;A useful &lt;strong&gt;“Not a Flutter port — here's why”&lt;/strong&gt; is much better than a generic Flutter tab that implies an implementation exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. What are the real numbers?
&lt;/h3&gt;

&lt;p&gt;Not just catalogue size.&lt;/p&gt;

&lt;p&gt;Ask for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;count per platform,&lt;/li&gt;
&lt;li&gt;what the denominator is,&lt;/li&gt;
&lt;li&gt;what is excluded,&lt;/li&gt;
&lt;li&gt;and how the number is produced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For KinetixUI today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98 / 98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Angular&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80 / 98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SwiftUI&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90 / 98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jetpack Compose&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90 / 98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90 / 98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The catalogue contains &lt;strong&gt;98 entries: 97 components and one documented recipe&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;90 of 98&lt;/strong&gt; are implemented across all four catalogue-complete platforms.&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;8 documented exceptions&lt;/strong&gt;, each with a reason.&lt;/p&gt;

&lt;p&gt;Angular deliberately sits outside that denominator because its catalogue is still incomplete and explicitly labelled &lt;strong&gt;Preview&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Folding Angular into the catalogue-complete parity figure would not make the system more truthful.&lt;/p&gt;

&lt;p&gt;It would just make the number harder to understand.&lt;/p&gt;

&lt;p&gt;Those numbers are generated from repository data.&lt;/p&gt;

&lt;p&gt;Edit the visible count by hand and the verification fails.&lt;/p&gt;

&lt;p&gt;That is also why some historical numbers earlier in this article are lower.&lt;/p&gt;

&lt;p&gt;The correction described above temporarily moved Compose from 90 to 89.&lt;/p&gt;

&lt;p&gt;Later, a real Compose Chart implementation moved it back to 90.&lt;/p&gt;

&lt;p&gt;Not because someone edited a marketing number.&lt;/p&gt;

&lt;p&gt;Because somebody built the missing thing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A generated number moves when the product moves.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the property I want.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that matters
&lt;/h2&gt;

&lt;p&gt;None of these guardrails are particularly exotic engineering.&lt;/p&gt;

&lt;p&gt;The platform-source check is roughly a couple hundred lines.&lt;/p&gt;

&lt;p&gt;At its core, it compares claims with files.&lt;/p&gt;

&lt;p&gt;The valuable part is where each check came from.&lt;/p&gt;

&lt;p&gt;The platform-source check exists because &lt;code&gt;direction-provider&lt;/code&gt; was wrong.&lt;/p&gt;

&lt;p&gt;The documentation checks exist because the Chart page advertised an API nobody had written.&lt;/p&gt;

&lt;p&gt;The block checks exist because a sentence about a single source sat above multiple independently typed copies.&lt;/p&gt;

&lt;p&gt;That leads to the only pattern I would strongly recommend:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When documentation turns out to be wrong, do not just fix the documentation.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What could have caught this before a reader did?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then build that check.&lt;/p&gt;

&lt;p&gt;And run it against everything else you have already published.&lt;/p&gt;

&lt;p&gt;Mine found four more problems.&lt;/p&gt;

&lt;p&gt;You probably will not enjoy that first run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it anyway.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;KinetixUI is an open-source design system built around one canonical DTCG token source. Tokens are generated into platform outputs; components are implemented natively per platform rather than transpiled from one shared runtime.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  See the evidence
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://kinetixui.com/docs/platforms?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=kx_p1_a_parity_proof&amp;amp;utm_content=art_primary" rel="noopener noreferrer"&gt;&lt;strong&gt;Explore the current generated platform coverage →&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The coverage page is derived from the component manifest and checked against real source rather than maintained as a marketing table.&lt;/p&gt;

&lt;p&gt;For the architecture behind it — shared tokens, native components, and what parity takes to verify — read:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kinetixui.com/guides/cross-platform-design-systems?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=kx_p1_a_parity_proof&amp;amp;utm_content=art_pillar" rel="noopener noreferrer"&gt;&lt;strong&gt;Cross-platform design systems: architecture, parity and trade-offs →&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>designsystems</category>
      <category>architecture</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your design tokens stop at the web boundary</title>
      <dc:creator>Zed Alleys</dc:creator>
      <pubDate>Tue, 06 Oct 2026 09:03:06 +0000</pubDate>
      <link>https://dev.to/zedalleys/your-design-tokens-stop-at-the-web-boundary-4p6a</link>
      <guid>https://dev.to/zedalleys/your-design-tokens-stop-at-the-web-boundary-4p6a</guid>
      <description>&lt;p&gt;There is a moment in every multi-platform design system where the pipeline quietly ends.&lt;/p&gt;

&lt;p&gt;It goes like this. You put your colours and spacing in a JSON file. You run a build step. Out comes a stylesheet full of custom properties, and every web component starts referring to &lt;code&gt;--color-surface&lt;/code&gt; instead of a hex code. This is good. It works. You have a single source of truth.&lt;/p&gt;

&lt;p&gt;Then the iOS app needs the same values.&lt;/p&gt;

&lt;p&gt;And here is where almost every design system does the same thing, including mine, at first: somebody opens the Swift project and types the numbers in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The copies are not the problem
&lt;/h2&gt;

&lt;p&gt;It is tempting to treat this as a discipline failure. It isn't. The engineer who typed those values into &lt;code&gt;Colors.swift&lt;/code&gt; made a completely reasonable decision — there was no other mechanism available, and the app needed to ship.&lt;/p&gt;

&lt;p&gt;The problem is structural: &lt;strong&gt;a value changed in the web pipeline has no route to a Kotlin constant.&lt;/strong&gt; Nothing connects them. There is no build step that fails, no diff that shows up in review, no test that goes red. The two files are related only by the fact that a person once made them agree.&lt;/p&gt;

&lt;p&gt;So they agree on Tuesday and disagree by the following quarter, and nobody can point to the commit where it happened, because there wasn't one. There were four commits, on four platforms, each locally correct.&lt;/p&gt;

&lt;p&gt;This is worth naming precisely because the usual response — "we should be more careful" — cannot work. Carefulness is not a mechanism. If the only thing keeping four platforms aligned is that everybody remembers to check, they will diverge the first week somebody is busy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "single source of truth" has to mean
&lt;/h2&gt;

&lt;p&gt;A source of truth that only one consumer reads is just a file.&lt;/p&gt;

&lt;p&gt;For it to be true across platforms, three things have to hold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every platform's values are generated from it.&lt;/strong&gt; Not copied from it. Generated, by a build step, into files that people do not hand-edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The generated files are visible.&lt;/strong&gt; Committed, diffed, reviewed. A token change should show up in a pull request as four files moving together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editing a generated file is an error you notice.&lt;/strong&gt; Otherwise the first urgent hotfix puts a hand-written value back into the pipeline and nothing tells you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third one is the one people skip, and it is the one that decides whether the system survives contact with a deadline.&lt;/p&gt;

&lt;h2&gt;
  
  
  DTCG, briefly, and why the format matters less than you think
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.designtokens.org/" rel="noopener noreferrer"&gt;Design Tokens Community Group&lt;/a&gt; format gives you a standard way to write tokens: a JSON structure where each token has a &lt;code&gt;$type&lt;/code&gt; and a &lt;code&gt;$value&lt;/code&gt;, and groups nest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"blue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"500"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#1b3c53"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"space"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dimension"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"16px"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The honest take: the specific format matters less than the fact that you have one. What DTCG buys you is a typed, tool-readable structure and an ecosystem that already knows how to transform it — most usefully &lt;a href="https://styledictionary.com/" rel="noopener noreferrer"&gt;Style Dictionary&lt;/a&gt;, which takes a token file and emits platform-specific output.&lt;/p&gt;

&lt;p&gt;What it does &lt;em&gt;not&lt;/em&gt; buy you is the interesting part. The interesting part is the layer you build on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layer that actually matters: primitives versus semantics
&lt;/h2&gt;

&lt;p&gt;Here is the mistake I made early, and I think it is the most common one.&lt;/p&gt;

&lt;p&gt;I generated &lt;code&gt;blue-500&lt;/code&gt; into every platform, felt good about it, and moved on. Six months later the design changed and I discovered that "blue 500" appeared in forty places across four platforms, and only some of them meant "the primary action colour". The rest meant "the colour of a link", or "the border of a focused input", or "that blue we use on the marketing site".&lt;/p&gt;

&lt;p&gt;A ramp of primitives is not a design system. It is a palette with build tooling.&lt;/p&gt;

&lt;p&gt;The layer that makes tokens survive a redesign is the &lt;strong&gt;semantic&lt;/strong&gt; one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;primitive:  color.azure.700  = #1d4ed8
semantic:   color.primary    = {color.azure.700}
            color.ring       = {color.azure.700}
            color.brand      = {color.blue.500}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;color.primary&lt;/code&gt; and &lt;code&gt;color.ring&lt;/code&gt; (the focus ring) happen to be the same value, and the day they need to stop being the same value, that is a one-line change rather than an archaeology project. It has already happened once: &lt;code&gt;primary&lt;/code&gt; used to be the navy that &lt;code&gt;color.brand&lt;/code&gt; still is, and moving it to a brighter blue for contrast changed an alias, not the components. Components refer only to semantics. Primitives are an implementation detail of the semantic layer.&lt;/p&gt;

&lt;p&gt;The practical test: &lt;strong&gt;can you answer "what is this colour for?" from the token name alone?&lt;/strong&gt; If the answer is "it's blue", you have a palette.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating for platforms that are not the web
&lt;/h2&gt;

&lt;p&gt;This is where the boundary gets crossed, and it is less work than it looks.&lt;/p&gt;

&lt;p&gt;Style Dictionary's model is: parse the tokens, apply transforms, run a format per target. A transform turns &lt;code&gt;16px&lt;/code&gt; into whatever that platform wants — &lt;code&gt;16.0&lt;/code&gt; for Swift, &lt;code&gt;16.dp&lt;/code&gt; for Compose, &lt;code&gt;16.0&lt;/code&gt; for Dart. A format decides what the output file looks like.&lt;/p&gt;

&lt;p&gt;Roughly, the four targets want:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;CSS custom properties&lt;/td&gt;
&lt;td&gt;Usually two sets, light and dark, toggled by a class or media query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;A Swift enum or struct of static constants&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Color&lt;/code&gt;, &lt;code&gt;CGFloat&lt;/code&gt;, and a way to express light/dark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;A Kotlin object of &lt;code&gt;Color&lt;/code&gt;/&lt;code&gt;Dp&lt;/code&gt;/&lt;code&gt;TextStyle&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Compose wants &lt;code&gt;Dp&lt;/code&gt; and &lt;code&gt;sp&lt;/code&gt;, not raw numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter&lt;/td&gt;
&lt;td&gt;A Dart class of constants&lt;/td&gt;
&lt;td&gt;Plus, ideally, a &lt;code&gt;ThemeData&lt;/code&gt; adapter — see below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dark-mode question is the one that catches people. On the web, light and dark are two sets of values behind one contract, and the contract is what components use. That same idea has to survive into native: you do not want &lt;code&gt;KinetixColors.blueLight&lt;/code&gt; and &lt;code&gt;KinetixColors.blueDark&lt;/code&gt; referenced directly in views. You want a semantic accessor that resolves by the current colour scheme, so the view code is identical in both.&lt;/p&gt;

&lt;p&gt;If your generated native output forces every view to branch on the colour scheme, the tokens have technically crossed the boundary and practically have not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The step almost nobody takes: make the tokens adoptable alone
&lt;/h2&gt;

&lt;p&gt;Here is the thing I did not expect, and it came from a real request rather than from planning.&lt;/p&gt;

&lt;p&gt;Someone wanted to use our Flutter tokens without our Flutter widgets.&lt;/p&gt;

&lt;p&gt;My first reaction was that this was a slightly odd thing to want. My second reaction, which was correct, was that it is the single most useful way into a design system, and that almost nobody offers it.&lt;/p&gt;

&lt;p&gt;Think about the position that team is in. They have an app. It has widgets. Those widgets work. What they do not have is agreement with the web app about what "surface" means. They do not need anybody's Button; they need the numbers.&lt;/p&gt;

&lt;p&gt;So we built a theme adapter — a function that turns the generated tokens into Flutter's own &lt;code&gt;ThemeData&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;theme:&lt;/span&gt; &lt;span class="n"&gt;KinetixMaterialTheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;light&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nl"&gt;darkTheme:&lt;/span&gt; &lt;span class="n"&gt;KinetixMaterialTheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, stock Material widgets inherit the design system. An &lt;code&gt;ElevatedButton&lt;/code&gt; you already had picks up the right colour, radius and type scale, and there is not a single custom widget in the tree.&lt;/p&gt;

&lt;p&gt;The equivalent exists for Cupertino, and the same idea generalises: on Android, a Compose &lt;code&gt;MaterialTheme&lt;/code&gt; wrapper; on iOS, an environment-injected theme. In KinetixUI those two are not built yet: today the stock-widget adapters are Flutter's, and the Compose and SwiftUI themes style KinetixUI's own components.&lt;/p&gt;

&lt;p&gt;What this changes strategically is the size of the first step. Adoption stops being a migration and becomes an afternoon. And the thing that actually drifts — the values, not the markup — gets fixed first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it honest
&lt;/h2&gt;

&lt;p&gt;Two checks are worth having from the start, and both are cheap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A contract test.&lt;/strong&gt; Assert that every semantic token exists on every platform's generated output. Not that the values match — transforms legitimately change them — but that the &lt;em&gt;names&lt;/em&gt; do. This catches the case where someone adds a semantic token, regenerates the web output, and does not notice the native targets were not part of that build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A no-hand-editing check.&lt;/strong&gt; Either regenerate in CI and fail on a dirty diff, or check the generated files against a fresh generation. This is the one that protects you from the urgent hotfix.&lt;/p&gt;

&lt;p&gt;In our repository both run on every pull request: a test that finds every role in the semantic colour contract in each platform's generated output and resolves it back to the source, and a regenerate-and-diff step. Alongside them is a contrast check over the colour pairs components render. That one resolves the source; the SwiftUI package repeats it against the generated Swift colours, which is the version with the interesting property: run against the output rather than the source, it catches a bad &lt;em&gt;transform&lt;/em&gt; as well as a bad value.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not solve
&lt;/h2&gt;

&lt;p&gt;Tokens are not components. Getting your spacing scale onto four platforms does not make a button on iOS behave like a button on the web, and it should not — a SwiftUI view that behaves like a React component is a bug, not a feature.&lt;/p&gt;

&lt;p&gt;Tokens also do not settle the harder governance questions: who is allowed to add a semantic token, what happens when a platform genuinely needs a value the others do not, how you deprecate. Those are people problems and a pipeline does not fix them.&lt;/p&gt;

&lt;p&gt;What a token pipeline does is narrow the problem to the parts that need human judgement, and remove the part that was never a judgement call in the first place: whether the number in the Kotlin file is the same number as the one on the web.&lt;/p&gt;




&lt;h3&gt;
  
  
  If you want to look at a working one
&lt;/h3&gt;

&lt;p&gt;KinetixUI's token pipeline is the one described here: a DTCG source, Style Dictionary, generated output for web, iOS, Android and Flutter, with the theme adapters and the CI checks. It is MIT, and the generated artifacts are committed, so you can read the output without building anything.&lt;/p&gt;

&lt;p&gt;The token contract, and what it generates: &lt;strong&gt;&lt;a href="https://kinetixui.com/docs/tokens?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=kx_p2_b_token_boundary&amp;amp;utm_content=art_primary" rel="noopener noreferrer"&gt;https://kinetixui.com/docs/tokens&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if the honest answer is that you only want the ideas and not the library — that is a completely legitimate outcome of reading this, and the Style Dictionary configuration is the part worth stealing.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>design</category>
      <category>ios</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
