Lesson 02 / 14
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.
Published Updated
In this lesson
- 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
Contents
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.
// 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.
// 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.
// 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.
// 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(" ")}`); }
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.
// 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));
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.
// 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");
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.
// 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));
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 @ androidrecord, even though the picker is a wheel on iOS and a calendar on Android, thepickercapability 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,
FrostedCardcame out degraded andDatePickerstayed 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.