---
title: 'Design System Adaptation'
source: 'https://academia.sh/en/courses/cross-platform-production/design-system-adaptation'
course: 'Cross-Platform Production Practice'
language: en
updated: '2026-10-21T05:02:51+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains the relationship between design tokens, components, and the design system'
    - 'Compares what adapting a component per platform gains and what it costs'
    - 'Measures that an adapted component works fully on both platforms'
    - 'Shows that the operability record stays the same when a token value changes'
---

# Design System Adaptation

Design system adaptation tunes one design language to the expectations of two platforms. The lesson measures that an adapted component works fully on both platforms and that another component stays degraded under a version condition.

In the previous lesson what changed by platform was a capability: when the layer did not fulfill the capability, the feature was disabled. In this lesson what changes by platform is appearance. A product has a single design language; its colors, spacings, corner radii, and type scales keep the product recognizable on both platforms. But iOS and Android users bring different habits from their own platforms, and a good design system adapts to this difference on purpose. The lesson first describes how an adaptation is built, then measures the operability of an adapted component.

## Design Tokens

A design system is the set of parts that keeps a product's appearance consistent: tokens, the components built from those tokens, and guidelines on when and how to use the components. A **design token** is the named value of a design decision: `accent` is a color, `gap` is a spacing, `radius` is a corner radius. A component does not write a color or spacing into itself; it reads the token by name. The gain is that a change happens in one place: the accent color changes in the token, and every component that reads it follows.

Tokens are thought of in three layers. The base layer carries the values the product keeps the same on both platforms. The adaptation layer carries the values written over the base on one platform. The component reads the final value from the combination of the two: if a value is defined on the platform it is used, otherwise the base value. This way the adaptation stays small, like a list of exceptions, and most of the product's values still live in one place.

The snippet below shows this reading on a component. The date picker takes its form and width from the token; it does not ask which platform it runs on. The snippet is not measured.

```jsx
// Platform code: component that reads tokens
function DatePicker({ tokens, value, onChange }) {
  const Picker = tokens.picker === "wheel" ? WheelPicker : CalendarPicker;
  return (
    <Picker
      value={value}
      onChange={onChange}
      width={tokens.width}
      radius={tokens.radius}
    />
  );
}
```

There is no platform check inside the component. The token decides which form is chosen, and the platform's adaptation layer fills in the token. This is the appearance-side counterpart of the previous lesson's layer boundary: the component says what it wants and does not know how the platform fulfills it.

## Adapting Per Platform

The reason for adapting is habit. Picking a date can be turning a wheel or opening a small calendar on iOS; on Android it is touching a day in a calendar dialog. Both users expect this on their own platform. Having the same component come out in a single form everywhere gives the product consistency but breaks the expectation the user brings from the platform. Adapting preserves the expectation and charges a price for it: the team now has to design, build, and test two forms.

This price should not be underestimated. Every adapted component is a testing multiplier: two forms on two platforms means two different widths and two different corner radii. For this reason a good design system chooses deliberately what to adapt. Components where the platform's own habit is strong, such as a date picker, are adapted; the colors and type scales that carry the brand's identity stay single.

Adaptation is not only appearance. Some components rely on a visual capability the platform offers, such as a blurred background. This capability does not exist on every platform and every version. In this case the component chooses one of two paths: it requires the capability and does not work without it, or it falls back to a plain background without it. The second path is a fallback, and the lesson measures it in the second half of the bounding measurement.

## From Token to Condition

The model builds the adaptation in two steps. First it keeps the tokens in a helper file. Then `themeFor(platform)` produces a condition from a platform's tokens: if the picker's form is defined, `picker` is in the condition's `has` list, and if the blurred background is supported, `blur` is. The lesson's core is given again below, as in every lesson.

```javascript
// operability.mjs — classifies how fully a feature works under a condition
export const LEVELS = ["full", "degraded", "deferred", "disabled"];
const FALLBACKS = ["degraded", "deferred"];

export function feature(name, needs, fallback = {}) {
  return { name, needs, fallback };
}

export function condition(name, has) {
  return { name, has };
}

function judge(item, cond) {
  const missing = item.needs.filter((need) => !cond.has.includes(need)).sort();
  let level = "full";
  for (const need of missing) {
    const given = FALLBACKS.includes(item.fallback[need]) ? item.fallback[need] : "disabled";
    if (LEVELS.indexOf(given) > LEVELS.indexOf(level)) level = given;
  }
  return { level, feature: item.name, condition: cond.name, missing };
}

export function assess(features, conditions) {
  const records = [];
  for (const cond of conditions) {
    for (const item of features) records.push(judge(item, cond));
  }
  return records;
}

export function printLevels(records) {
  for (const r of records) {
    const missing = r.missing.length === 0 ? "-" : r.missing.join(",");
    console.log(`${r.level}: ${r.feature} @ ${r.condition} missing=${missing}`);
  }
}

export function countLevels(records) {
  for (const name of LEVELS) {
    console.log(`${name}: ${records.filter((r) => r.level === name).length}`);
  }
  console.log(`affected: ${records.filter((r) => r.level !== "full").length}`);
}
```

The number of the nine-record opening measurement was given in the first lesson; this lesson does not change it and only measures its own slice. The tokens and `themeFor` live in a lesson-local helper file. There are three adaptations: iOS, Android, and an old Android that stands for the version condition.

```javascript
// tokens.mjs — the values of one design language that are adapted per platform
import { condition } from "./operability.mjs";

const BASE = { accent: "blue", gap: 8, radius: 4, blur: true };

const ADAPT = {
  ios: { picker: "wheel", radius: 10, width: 280 },
  android: { picker: "calendar", width: 320 },
  "android-legacy": { picker: "calendar", width: 320, blur: false },
};

export function tokensFor(platform) {
  return { ...BASE, ...ADAPT[platform] };
}

export function themeFor(platform, tokens = tokensFor(platform)) {
  const has = [];
  if (tokens.picker) has.push("picker");
  if (tokens.blur) has.push("blur");
  return condition(platform, has);
}
```

The first measurement prints the tokens, not the operability. The final values of the two platforms are written with sorted keys.

```javascript
// main.mjs
import { tokensFor } from "./tokens.mjs";

for (const platform of ["ios", "android"]) {
  const tokens = tokensFor(platform);
  const keys = Object.keys(tokens).sort();
  console.log(`${platform}: ${keys.map((k) => `${k}=${tokens[k]}`).join(" ")}`);
}
```

```text
ios: accent=blue blur=true gap=8 picker=wheel radius=10 width=280
android: accent=blue blur=true gap=8 picker=calendar radius=4 width=320
```

The adaptation layer carries only the difference. `accent` and `gap` come from the base on both platforms, `radius` is overridden only on iOS, and `picker` and `width` carry their own value on both platforms. All of these values are the lesson's construction and carry no unit.

**SA3.** The token values (`gap`, `radius`, `width`) are the lesson's construction; they are not the measurements of a real design system. From a token the model draws only two conclusions: whether the picker's form is defined, and whether a blurred background is supported. How the component is actually drawn and how the two forms feel to the user are not in the model. The model builds this operability's rule; the model is not the framework itself.

## The Bounding Measurement

The lesson's focus record is `DatePicker @ android`. `DatePicker` needs only the `picker` capability. The tokens differ on the two platforms: a wheel on iOS, a calendar on Android. But both have a picker form, so the capability exists under both conditions.

```javascript
// main.mjs
import { feature, assess, printLevels } from "./operability.mjs";
import { themeFor } from "./tokens.mjs";

const features = [feature("DatePicker", ["picker"])];
const conditions = [themeFor("ios"), themeFor("android")];
printLevels(assess(features, conditions));
```

```text
full: DatePicker @ ios missing=-
full: DatePicker @ android missing=-
```

`DatePicker @ android` is full. This measurement differs from the question of M32/K03. That lesson asked who owns a component's width during the layout pass. When a design system adapts a component per platform its width can change; operability does not classify the width but whether the component does its job under that condition. On the two platforms the picker is drawn in two different widths and two different forms, and it does its job in both.

The second measurement shows directly that the rule does not see token values. On the Android adaptation the corner radius, the width, and the picker's form are changed; the records before and after are compared.

```javascript
// main.mjs
import { feature, assess } from "./operability.mjs";
import { tokensFor, themeFor } from "./tokens.mjs";

const features = [feature("DatePicker", ["picker"])];
const before = assess(features, [themeFor("android")]);
const edited = { ...tokensFor("android"), radius: 16, width: 360, picker: "list" };
const after = assess(features, [themeFor("android", edited)]);
console.log(JSON.stringify(before) === JSON.stringify(after) ? "record unchanged" : "record changed");
```

```text
record unchanged
```

Three tokens changed and the record stayed the same. The rule sees only what enters `needs`, `fallback`, and `has`; a change that does not show up in the capability set does not disturb the record. This is not a contradiction but the measure itself: a narrow or wide width may affect the user, but the model says only whether the picker does its job.

The second comparison is a component that carries a fallback. `FrostedCard` wants a blurred background; without blur it falls back to a plain background. A condition for an old Android version that does not fulfill blur is added.

```javascript
// main.mjs
import { feature, assess, printLevels } from "./operability.mjs";
import { themeFor } from "./tokens.mjs";

const features = [
  feature("DatePicker", ["picker"]),
  feature("FrostedCard", ["blur"], { blur: "degraded" }),
];
const conditions = [themeFor("ios"), themeFor("android"), themeFor("android-legacy")];
printLevels(assess(features, conditions));
```

```text
full: DatePicker @ ios missing=-
full: FrostedCard @ ios missing=-
full: DatePicker @ android missing=-
full: FrostedCard @ android missing=-
full: DatePicker @ android-legacy missing=-
degraded: FrostedCard @ android-legacy missing=blur
```

Under the old version condition `DatePicker` is still full, while `FrostedCard` is degraded: blur is missing, and the feature works immediately with a plain background. Five of the six records are full. This number is not the number of the opening 5/1/2/1 measurement; the two measurements count different sets.

**SA4.** That the old version condition does not fulfill blur is the lesson's construction. The model does not say which real version carries this capability; for this reason no version number is written in the text. How bad or good the plain background looks to the user is also not in the model.

A fallback is the design decision that turns disabled into degraded. If `FrostedCard`'s `fallback` map were empty, the same missing capability would give a disabled result. The design system writes this decision into the component: when the component cannot find the capability, it already knows what to do.

## Summary

- This lesson's operability: full. In the `DatePicker @ android` record, even though the picker is a wheel on iOS and a calendar on Android, the `picker` capability exists under both conditions and nothing is missing.
- A design token is the named value of a design decision; the adaptation layer writes only the platform difference over the base.
- Even when the width, the corner radius, and the picker's form changed, the record stayed the same; the rule does not see a change that does not show up in the capability set.
- Under the old version condition that does not fulfill blur, `FrostedCard` came out degraded and `DatePicker` stayed full.
- Adaptation preserves the expectation and creates a testing multiplier; a fallback is the design decision that turns disabled into degraded.

## Next Step

In this lesson the content of the token determined the capability. The next lesson, `Configuration and Environment Management`, covers the case where the capability comes from configuration: development, staging, and production environments, secrets, and the value of a flag on a first launch when remote configuration cannot be reached.
