Mobile Computing · Cross-Platform Development

One Description, Two Design Languages

Material and Apple design languages differ in elevation, corner treatment, motion, and safe-area insets. A shared description has to keep semantics while letting the visual layer diverge per platform.

A cross-platform application has one product design and two platform design languages. Material defines elevation as a shadow plus a tonal surface tint, minimum touch targets as a density-scaled unit, and motion as a set of standard curves and durations. Apple’s guidelines define depth through layering and translucency, treat the safe area as an inset rectangle the system owns, and specify motion in terms of continuity and interruptibility rather than fixed durations.

Neither language is a superset of the other, so a shared description cannot be expressed once in either vocabulary. It has to be expressed in a vocabulary that is neutral between them, with per-platform bindings that supply the visual interpretation. The neutral part is the semantics of the interface; the divergent part is how it looks and moves. Keeping that line clean is the whole problem.

Semantics survive translation, style does not

The useful decomposition is 3 layers: intent, structure, and presentation.

Intent is what the screen is for: a list of transactions, each with an amount, a date, and a state; a detail screen for one transaction; a destructive action that requires confirmation. Intent does not mention a tab bar, a floating action button, or a navigation bar, because those are Material and Apple answers to the same intent.

Structure is the arrangement of information: which elements are grouped, which are primary, which are repeated, and what the reading order is. Structure is largely shared, with the qualification that it must be expressed in logical rather than visual terms so that a different visual arrangement can satisfy it.

Presentation is the platform binding: the bar style, the elevation, the corner radius, the transition, the ripple or the highlight, the placement of the primary action. Presentation is where a shared description should defer, not compromise.

The failure mode of a shared design system is a fourth layer that tries to unify presentation. It produces a component that is neither Material nor Apple, which reads as foreign on both platforms and is usually justified by brand consistency. Brand consistency is a real requirement, and it is satisfiable at the layer of colour, typography, and imagery without reimplementing the interaction and elevation conventions of either platform.

Safe areas are geometry the system owns

The clearest example of a difference that cannot be averaged is the safe area.

On iOS, the safe area is a rectangle inset from the screen edges to avoid the notch or dynamic island, the home indicator, and the status bar. The insets change with device orientation and with whether a bar is translucent, and the system propagates them into the view hierarchy. A view that ignores them draws under the home indicator and becomes hard to interact with at the bottom edge.

On Android, the equivalent information arrives as window insets: system bars, display cutout, and, on recent versions, the insets for the gesture navigation area. The insets are delivered separately for each type and each edge, and the application decides which to consume. An application that consumes the status bar inset by padding its content also needs to draw behind the bar, which is a different composition than iOS’s default.

The shared layout must therefore treat insets as data supplied by the adapter, not as constants. A screen description says “content respects the safe area” or “this image extends under the status bar and the content below it respects the safe area”, and the adapter supplies four numbers per edge. The numbers are device-specific and change at run time when the keyboard appears, when the device rotates, or when a call arrives and the status bar changes height.

The concrete magnitudes matter for testing: a status bar is roughly 24 density-independent units on many Android devices and 44 to 59 points on recent iPhones depending on the model, and the home indicator region is around 34 points. These are approximate and model-dependent, which is exactly why they must be queried rather than hard-coded.

Density is not a scale factor

The second un-averageable quantity is density. Android defines a density-independent pixel as one pixel on a 160 dpi baseline, with the device reporting a scale factor such as 2.75 or 3.0. iOS defines a point as one pixel on the original non-retina baseline, with scale factors of 2 or 3 on current devices. The two baselines are numerically similar and semantically different: a 48-unit Android touch target and a 44-point iOS touch target are both “the standard minimum”, and they are not the same physical size on the same screen.

Converting one to the other by ratio produces layouts that are slightly wrong everywhere. The correct treatment is to express the shared layout in abstract units that the adapter converts using the platform’s own scale, and to accept that the same abstract unit yields different physical sizes. The acceptance is not a compromise; it is fidelity, because the platform’s own applications use the platform’s own unit.

The exception is content whose physical size matters: a barcode scanner viewfinder, a photograph, a drawing canvas. Those should be expressed in physical units derived from the display’s reported density, not in platform-independent units, because the platform-independent unit is defined relative to a nominal density rather than to the actual screen.

Text is the third case and it does not follow density at all. Text follows the user’s font scale setting, which is independent of density on both platforms and is exposed separately. A layout that derives text size from density will ignore the accessibility setting; one that derives it from the font scale will honour it. The two multipliers compose, and the shared layout has to keep them distinct rather than collapsing them into one number. What happens after the size is chosen, from shaping to line breaking, is covered in Text Metrics and Line Breaking Across Two Font Stacks.

Motion: durations versus interruptibility

Material specifies motion in terms of duration ranges and easing curves tied to the size of the transition: a small element expands over a short interval, a full-screen transition over a longer one, with standard easing for entering and emphasized easing for exits. Across the standard set the durations span roughly 100 to 500 ms, with the largest transitions at the top of that range. Apple’s guidance is less about fixed durations and more about properties: motion should be interruptible, should preserve continuity of position, and should feel like it responds to the gesture rather than playing after it.

A shared animation description that specifies a duration and a curve satisfies Material and fights Apple. The neutral description is a set of properties: what moves, from where to where, whether it is interruptible, and whether it is reversible mid-flight. The adapter then maps those properties onto its platform’s animation system, choosing a duration that fits the platform’s conventions.

The observable difference is in gesture-driven transitions. A back gesture on iOS animates under the user’s finger and reverses if the gesture is cancelled; a Material transition on Android, until predictive back, played after the decision. Expressing the transition as “interruptible, reversible, driven by progress in the range zero to one” allows both: the iOS adapter drives it from the gesture’s progress, and the Android adapter drives it from the predictive back progress, or plays it linearly where predictive back is unavailable.

Theme tokens versus component reimplementation

The mechanism that keeps the shared description neutral while allowing per-platform presentation is a token layer: named values that the platform binding resolves.

type TokenSet = {
  colorSurface: string;
  colorOnSurface: string;
  elevationRaised: 'shadow' | 'tonal' | 'border';
  cornerRadiusControl: number;
  motionEnterMs: number;
  minTouchTarget: number;
};

const material: TokenSet = {
  colorSurface: '#FEF7FF',
  colorOnSurface: '#1D1B20',
  elevationRaised: 'tonal',   // Material 3 expresses elevation with a tint
  cornerRadiusControl: 20,
  motionEnterMs: 200,
  minTouchTarget: 48,
};

const apple: TokenSet = {
  colorSurface: '#FFFFFF',
  colorOnSurface: '#000000',
  elevationRaised: 'shadow',
  cornerRadiusControl: 10,
  motionEnterMs: 250,
  minTouchTarget: 44,
};

The values above are illustrative of the shape rather than a specification of either system’s palette; the point is that the token is resolved per platform and the component code reads the token. A component that branches on a platform identifier to choose a shadow has hard-coded the design language into the component, and the branch count grows with every component.

Tokens have a failure mode of their own: they become a shadow design system. Once a token set has 200 entries, the shared description is describing presentation in detail and the platform binding is a lookup table, which is the fourth layer again with extra steps. The discipline is to keep tokens at the level of decisions the platform genuinely makes differently, and to let the platform’s own component supply everything else.

Choosing the platform component where one exists

The highest-fidelity option, where the architecture allows it, is not to draw the component at all. A date picker, a share sheet, a document browser, an activity indicator, and a text field’s selection behaviour are platform components whose behaviour users know. Wrapping them behind the shared interface gives full fidelity for free, at the cost of an interface that can only express what both can do. Whether that is available at all depends on who owns the scene graph, which is the split examined in Retained Native Widgets or an Owned Render Tree.

The interface for a date picker, for example, should not promise a spinner or a calendar grid; it should promise that a date can be chosen and report the chosen date. Which presentation appears is the platform’s decision, and both platforms have a defensible answer. A wrapped component inherits 3 things a drawn one has to rebuild: its presentation, its interaction behaviour, and its accessibility tree.

Where a component must be drawn, because the design requires it or because neither platform offers it, the drawn version inherits the obligation to satisfy the platform’s interaction conventions: the touch target minimum, the focus behaviour, the accessibility roles discussed in Bridging One Semantics Tree to Two Accessibility APIs, and the platform’s own press feedback. A drawn button that highlights instantly on Android and does not highlight on iOS has taken the worst of both.

Testing the two bindings

Tests fall into 2 kinds, and they check different things.

Structural tests run against the neutral description and assert the intent and structure: the screen contains a list of transactions, each row has an accessible label composed of amount and date, the destructive action is reachable and labelled. These run once and apply to both platforms, and they catch the regression where a refactor changed the information architecture.

Visual tests run per platform and compare rendered output against a reference image captured on that platform. They catch the binding regressions: an inset that stopped being applied, a token that resolved to the wrong value, a corner radius that changed. Reference images are per-platform by necessity, since the whole point is that the two platforms look different, and they need to be regenerated deliberately when the design changes rather than automatically on failure.

The gap between the two is where the interesting bugs live: a layout that is structurally correct and visually plausible but that puts a control below the home indicator on one device model. Neither test catches that unless the visual test runs on that model, which is why the visual matrix should include the devices with the largest insets rather than the most common ones.

References

  1. Material Design 3: Layout — Google
  2. Apple Human Interface Guidelines: Layout — Apple
  3. Android Documentation: Window insets and display cutouts — Android Developers
  4. WCAG 2.2: Target Size (Minimum) — W3C