The handoff is a lossy codec
A handoff is a lossy compression of design intent, and the person in the middle is the decompression algorithm — which is why output fidelity depends on who happens to be on shift that week.
I spent years on the compression side of that pipeline, managing UI teams and writing specifications precise enough to survive a translation. The specifications were not the problem. The problem was that every translation step had a human in it, and every human optimises for finishing their own step.
Design engineering is not "designers who can code" or "engineers who care about kerning". It is the decision to delete the translation step entirely: one person owns intent from the frame to the deployed pixel, and the artefacts in between are machine-readable rather than prose.
What actually gets lost between a frame and a build
| Intent | What the file stores | What ships | Who notices |
|---|---|---|---|
| Vertical rhythm | Frame positions, often nudged by hand | Per-component margins that do not compose | Nobody, until two components stack |
| Type scale | Nominal sizes in the desktop frame | One size, no fluid behaviour | Mobile users, at every breakpoint |
| Interaction states | A hover variant, sometimes | Focus ring from the browser default | Keyboard users, accessibility audits |
| Motion | A prototype that plays on click | transition: all 0.3s ease | Everyone, as jank on cheap Android |
| Empty and error states | Not drawn | Whatever the API returns, unstyled | Support tickets, week two |
| Long content | Lorem ipsum at a comfortable length | Wrapped German compound nouns breaking the grid | Localisation, quarterly |
The pattern is consistent: frames encode the happy path at one viewport, and production is mostly the unhappy paths at nine viewports. A handoff does not fail loudly. It fails as a slow accumulation of nearly-right decisions, and the cost shows up as review cycles rather than bugs.
In practice, one full specification-to-implementation round trip on a mid-complexity screen — a settings panel with a dozen controls — costs between four and ten hours across two people, and the second round trip exists mostly to fix what the first one lost. Multiply by the number of screens and you have the actual price of the process: not the salary delta of a hybrid role, but the compounding cost of every screen being touched twice.
Design tokens are the interface between design and code
If there is one thing to get right, it is this: the contract between design and code should be a file, versioned in the repository, in a format both sides can generate from.
Use the W3C Design Tokens Community Group format as the source of truth. It is boring, portable, and readable by every serious tool.
{
"$schema": "https://design-tokens.org/schema.json",
"color": {
"surface": {
"base": { "$type": "color", "$value": "oklch(0.98 0.003 250)" },
"raised": { "$type": "color", "$value": "{color.surface.base}" },
"inverse": { "$type": "color", "$value": "oklch(0.19 0.012 260)" }
},
"accent": {
"default": { "$type": "color", "$value": "oklch(0.62 0.14 250)" },
"contrastText": { "$type": "color", "$value": "{color.surface.base}" }
}
},
"space": {
"2": { "$type": "dimension", "$value": "0.5rem" },
"4": { "$type": "dimension", "$value": "1rem" },
"6": { "$type": "dimension", "$value": "1.5rem" }
},
"radius": {
"control": { "$type": "dimension", "$value": "6px" },
"panel": { "$type": "dimension", "$value": "14px" }
}
}
Two rules make this work rather than become ceremony. First, semantic names, never appearance names: surface.raised, not gray-100. Second, every token exists in both the design library and the code theme, generated from this file — the design file imports the JSON through a plugin, the CSS is generated at build time. Neither side hand-maintains the other's copy.
With Tailwind CSS v4 the code side is a short theme block, which is the point: the token file is the input, the theme is a generated view of it.
/* app/theme.css — generated from tokens/design.tokens.json */
@import "tailwindcss";
@theme {
--color-surface-base: oklch(0.98 0.003 250);
--color-surface-raised: oklch(0.99 0.002 250);
--color-surface-inverse: oklch(0.19 0.012 260);
--color-accent-default: oklch(0.62 0.14 250);
--spacing-2: 0.5rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
--radius-control: 6px;
--radius-panel: 14px;
--text-body: clamp(0.95rem, 0.92rem + 0.15vw, 1.0625rem);
--text-body--line-height: 1.65;
--text-display: clamp(1.75rem, 1.2rem + 2.4vw, 3rem);
--text-display--line-height: 1.08;
--text-display--letter-spacing: -0.022em;
}
| Token distribution | Drift risk | Effort per change | What breaks first |
|---|---|---|---|
| Hand-maintained on both sides | High | Two edits, one review | Colour ramps, quietly |
| One-way export plugin from the design tool | Medium | One edit plus a re-export | Token names, on the first rename |
| DTCG JSON in the repo, generated both ways | Low | One pull request | The generator, loudly, in CI |
The third row is the one I use. When someone edits the token file without regenerating the CSS, CI fails on a diff. That failure is cheap and immediate; the equivalent drift discovered in a design review three weeks later is not.
Type and space are functions, not values
A fixed type scale is a design decision that only holds at one viewport. clamp() with a modular ratio scaled by viewport unit is one line of CSS that removes an entire class of breakpoint-specific fixes.
Two constraints keep fluid type honest. Line height must decrease as size increases — a 1.7 line-height is generous at 16px and absurd at 48px, so derive it per step rather than globally. And long-form measure belongs between 60 and 75 characters, which is best expressed in ch rather than in pixels, because ch survives a font-size change that pixels do not.
The spaces between type sizes matter more than the sizes. A ratio of 1.25 for body-adjacent steps and 1.333 for display steps gives you a scale where a designer can pick from the set without inventing a value mid-review. When a value is genuinely missing, the correct move is to add it to the token file in the same pull request as the component, not to inline a magic number that the next person will find in six months and be afraid to touch.
Accessibility as a compile-time constraint
Contrast, focus visibility, motion preference, and target size are not review topics. They are lint rules that run before a human looks at anything.
- Normal text against its actual background: 4.5:1 minimum under WCAG 2.2 AA; 3:1 for large text and for UI component boundaries.
- Focus visible: never
outline: nonewithout a replacement that has 3:1 contrast against the adjacent colour. - Target size: 24 × 24 CSS pixels minimum for pointer targets.
- Motion: honour
prefers-reduced-motionwith a real reduction, not a shorter duration.
# CI: structural and contrast checks on the built output, not on the source
pnpm exec playwright test tests/a11y.spec.ts --project=chromium
pnpm exec axe --exit --tags wcag2a,wcag2aa --include "main" .next/static
pnpm exec lighthouse-ci autorun --collect.staticDistDir=.next --assert.assertions.categories:accessibility=1
Automated tooling catches somewhere between a third and a half of real accessibility defects — it will find a missing label, not a confusing one. Treat a green run as permission to review, never as evidence of correctness. The defects that matter are semantic: heading order that matches visual hierarchy, a <button> where an <a> belongs, a form error announced rather than merely coloured red.
The engineering value of these rules is that they are checkable. "Make it feel accessible" is not a constraint anyone can satisfy consistently. "3:1 against the adjacent surface, verified in CI" is.
Performance budgets are design constraints
Performance is not a later optimisation pass; it is a set of numbers that constrain what a design can be. A hero video with three typefaces and a 600KB illustration is not a design that can be made fast later. It is a design that has already spent the budget.
| Metric | Budget | What blows it | The design decision that fixes it |
|---|---|---|---|
| LCP | Under 2.0s on 4G | A hero image served uncompressed | Reserve the box; serve AVIF under 120KB |
| INP | Under 200ms | Scroll-linked animation on a low-end phone | Animate transform and opacity only |
| CLS | Under 0.02 | Late-loading fonts and consent banners | Size-adjusted fallback metrics; reserved banner space |
| Route JS | Under 170KB gzip on article routes | A component library imported wholesale | Ship the four components the page uses |
| Fonts | Two subsets, under 40KB each | Four weights of a latin+CJK display face | Variable font, subset, font-display: swap |
The productive version of this conversation happens while the frame is still open. It is much easier to say "that illustration needs to be out of the LCP path" than to remove it after the client has approved the visual.
What replaces the handoff in practice
Three working agreements carry most of the value.
Designers review on the deploy preview, not on a static frame. A preview link on a real device catches more than an annotated mockup, and it costs a push instead of a meeting.
The token file is the only place a new visual value can be introduced. Everything else references it. This is a one-line rule that eliminates most review arguments about numbers.
The same person who draws the interaction implements it, or sits beside the person who does, in the same pull request. Not because collaboration is virtuous, but because the fidelity loss happens at the boundary, and the cheapest fix for a boundary is to remove it — the same argument I make about the typed tool boundary in an AI workflow.
Closing: fewer artefacts, higher fidelity
Design engineering is not a hybrid job title, and it is not the abolition of design review. It is the recognition that every artefact between intent and production is a place where the intent degrades, and that the cheapest way to protect fidelity is to need fewer artefacts. Tokens replace specifications, deploy previews replace annotated frames, and CI replaces the argument about whether the contrast is acceptable. The work that remains is the part that was always the actual work: deciding what the interface should be, and then being the person who makes it exist exactly that way. It is also how I run the design engineering engagements I take on.
Keep reading
- CJK Typography on the Web: Why Tracking Breaks Chinese Text2026-03-039 minDesign Engineering
- Accessible Form Patterns Are Four Primitives, Not a Component Library2026-02-259 minDesign Engineering
- Automated Contrast Testing Has to Resolve Colour in a Browser, and Fail Closed When It Cannot2026-02-108 minDesign Engineering
- Design Tokens as Typed Code: If the Build Does Not Consume It, It Is Documentation2026-02-287 minDesign Engineering
- Evolve the Ecosystem, Weave the Garden: The Architecture of a Super Individual2026-01-128 minSuper Individual