A game companion app's design system ships thin wrappers for three UI frameworks, and months later the same item picker behaves differently in each; what went wrong, and how do you stop wrapper drift?
answer
- hand-written, team-owned wrappers
- defaults and behaviour crept outward
- one machine-readable component contract
- one conformance suite, run per wrapper
- release core and wrappers together
basics
~20 sWrapper drift happens when hand-written wrappers gain their own options, defaults and behaviour. Stop it with one machine-readable component contract that generates or type-checks every wrapper, a shared conformance suite run against each, and lockstep releases.
solid answer
~50 sThe wrappers stopped being thin. Each was hand-written, often by the team using that framework, so fixes landed in one wrapper instead of the core, one wrapper added its own size option, another declared its own default variant, and one emitted its change event at a different moment. To stop it: describe every component once in a **machine-readable contract** — options, allowed values, defaults, events, slots — and generate the wrappers from it or fail CI when a wrapper's public API differs from it; enforce a **thin-wrapper rule** (no behaviour, defaults or styling in wrappers); run one **conformance suite**, written against a framework-neutral driver, against every wrapper; and **release the core and all wrappers together** under one owner. Then audit the existing divergences and treat aligning each one as a versioned change for that wrapper's consumers.
go deeper
Recall what wrapper drift is: the same component behaving or configuring differently in each framework because each wrapper grew its own options, defaults or behaviour.
Explain the mechanisms that stop it: one component contract, generated or checked wrappers, a thin-wrapper rule, and a conformance suite shared across frameworks.
Walk through a real remediation: audit the divergences, classify bug versus feature versus idiom, and roll out alignments as announced breaking changes for the affected wrapper's consumers.
Treat drift as a structural cost of N wrappers and weigh it against fewer frameworks or a neutral delivery format, not as a staffing issue.
## The symptom A multiplayer game's companion app has a design system with an agnostic core and thin wrappers for three UI frameworks: the clan portal, the match-stats dashboard and the event site. Months later, the **item picker** used for loadouts no longer agrees with itself: - the clan-portal wrapper grew an extra-large size option nobody else has; - the dashboard wrapper defaults to the secondary variant while the core contract says primary; - the event-site wrapper emits its change event on every keystroke instead of when an item is chosen. Bugs are filed against "the design system", reproduce in one framework only, and get fixed in one wrapper. That is **wrapper drift**: the per-framework layers have become three slightly different components with one name. ## Why wrappers drift - **They are hand-written**, so every wrapper re-declares the API and each declaration can differ. - **Framework teams own their own wrapper** and add what they need locally instead of proposing it to the core. - **Fixes land where the bug was reported**, so a behaviour fix reaches one framework and not the others. - **Defaults and behaviour creep outward** from the core into wrappers because it is the quickest place to change them. - **Nothing compares the wrappers**: each has its own tests, written by its own team, checking its own behaviour. - **Releases are independent**, so wrappers sit on different core versions at the same time. ## Finding the drift that is already there 1. **Extract each wrapper's public API** — option names, types, allowed values, defaults, events — and diff the three against the core contract. 2. **Run the same interaction scenarios** against all three (choose an item with the keyboard, clear it, open and dismiss) and compare the outcomes and the resulting accessibility tree. 3. **Classify every difference**: a bug in one wrapper, a missing feature, or an intentional framework idiom that should be documented. 4. **Fix the bugs first**, then decide each feature difference in the core: adopt it for everyone or remove it. ## Preventing it | Mechanism | What it stops | |---|---| | **One machine-readable component contract**, with wrappers generated from it or checked against it in CI | Options, values and defaults declared differently per framework | | **Thin-wrapper rule** enforced in review: no behaviour, defaults or styling in a wrapper | Logic creeping out of the core | | **Shared conformance suite** written once against a framework-neutral driver that queries by role and accessible name, run against every wrapper | Behaviour differences, such as when a change event fires | | **Lockstep releases**: core and all wrappers carry one version and ship together | Wrappers sitting on different core versions | | **One owning team** for all wrappers, with feature requests going to the core contract | Local additions made for one team | The contract is the load-bearing part. When the item picker's options live in one description, adding the extra-large size becomes a single change reviewed once and appearing in every framework, rather than an edit to one wrapper that the other two never see. ## Aligning the ones that diverged Aligning a drifted wrapper is not free. Apps built on the dashboard wrapper rely on its secondary default; switching it to primary changes their screens without any code change on their side. Removing the clan portal's extra-large size breaks the screens that use it. Each alignment is a breaking change *for that wrapper's consumers*, so it is announced, versioned and, where possible, shipped with a migration note or an automated fix. The other two wrappers' consumers see no change, because they already match the core. ## The judgment underneath Drift is less a people problem than a structural one: it is what happens when N copies of an API exist and nothing makes them the same. The fix is structural: **one description of the API, one description of the behaviour, one test suite, one release**, with wrappers reduced to translation. A wrapper that is neither generated nor checked tends to drift again.
- What belongs in the machine-readable component contract?Every part of the public API a consumer can depend on: option names, types and allowed values, defaults, events and when they fire, slots or content regions, and descriptions for documentation. From it you can generate wrappers and typings, diff each wrapper's surface in CI, and render the API table in the docs, so one edit updates all of them.
- How can a conformance suite be written once and run against three frameworks?Write the scenarios against a framework-neutral driver: mount the component through a small per-framework adapter, then find elements by role and accessible name and dispatch real user input. The scenario — choose an item with the keyboard, expect one change event — never mentions the framework, so the same file runs against every wrapper and any difference fails the build.
saying these in an interview costs you the question
- Each framework team should own and extend its own wrapper as it needs.
- Wrappers may set their own defaults to match their framework's conventions.
- Per-wrapper test suites written by each team are enough to catch drift.
- Aligning a drifted default needs no announcement, since it only restores the contract.
- Drift is a communication problem that better meetings will solve.