Design engineering is the discipline that owns the seam between Figma and production. What a design engineer does, the core skill of negotiating fidelity, and how to grow into the role.
For six years I called myself a frontend developer, and I was a good one — hand me a Figma file and I'd hand you back pixels that matched. Then during a redesign at Shpper, a designer said one sentence that quietly rearranged my whole idea of the job: "You keep making the decisions I forgot to make."
She wasn't complaining. She was describing a role that had no name on our org chart, no line in my job description, and by that point most of my calendar. That gap between what a designer draws and what actually ships — the empty space where a thousand tiny decisions live — turned out to be a real discipline. I spent a year falling into it before I understood I'd stopped being a frontend dev. I'd become a design engineer. The strange part: the skill that made me good at it had almost nothing to do with CSS.
Before I go further, a definition, because the title gets thrown around loosely. A design engineer is the person who owns the translation layer between design intent and shipped product. Not a "senior frontend developer with a nicer title," not a "designer who codes a little" — a distinct discipline whose core job is resolving the ambiguity between a static mock and a live, stateful, network-dependent interface running on a real device in someone's hand.
If you've seen the term UX engineer, UI engineer, or design technologist, they're pointing at roughly the same seam. The label matters less than the ownership: someone has to be accountable for the space between "matches Figma" and "correct in production," and on most teams nobody is. Design engineering is what happens when you make that ownership explicit.
A Figma file is a set of assertions about a happy path. One screen, one state, perfectly-sized data. Real products are the exact opposite: loading states, empty states, error states, a display name that's 40 characters long, a network that drops mid-transaction, a keyboard that swallows the submit button, a user on a five-year-old Android in Dubai sunlight where your subtle grey-on-grey contrast is completely invisible.
Nobody drew those. Somebody has to decide them. For a long time that somebody was "whoever built the screen," making silent judgment calls at 6pm against a deadline. The results were inconsistent because the decisions were invisible — never discussed, never reviewed, just quietly baked into whichever widget got written that afternoon. Two engineers would solve the same empty-state problem three different ways in the same app and nobody noticed until a designer scrolled through and winced.
At some point I looked at my week and realized most of it was spent in exactly that territory. Not writing new UI — resolving the ambiguity between the mock and the machine. What does this list look like with zero items, with one, with a thousand? When the API takes 1.8 seconds, do we show a skeleton or a spinner, and how long before "loading" starts to read as "broken"? That work needed an owner. It turned out to be me, and once I admitted that out loud, the job got a lot clearer.
Designers own intent. Backend and app engineers own the system and its constraints. The design engineer owns the translation layer — and translation is exactly where meaning gets lost or preserved.
Concretely, on a typical feature I own:
None of this belongs cleanly to the designer or the engineer. It's a seam. Owning the seam is the whole job.
The reason design engineering is a distinct discipline and not just "senior frontend with a nicer title" is that you have to be fluent in two languages that describe the same object with completely different words. You are a translator, and a translator who only speaks one language well is useless in the exact moment it matters.
In the design room, people talk about hierarchy, rhythm, optical alignment, contrast, affordance, and "does this feel heavy." In the engineering room, people talk about reflow, layout passes, constraints, z-index, rebuild cost, and "this list drops frames on scroll." A design engineer holds both vocabularies at once, because the interesting problems live precisely where they collide.
One that comes up constantly: a designer wants a blur-heavy frosted-glass panel floating over a scrolling background. In their room, it's a texture choice — it makes the screen feel premium. In my room, it's a per-frame GPU cost that can turn a smooth 60fps list into a stuttering 40 the moment the user flicks it. The conversation only works if I can say, "That blur costs us frames on this scroll surface — here's a version that reads 90% as premium and stays smooth," and mean something precise on both sides of that sentence. If I can only speak one language, I either ship a janky product or I say "no" with no reason attached, and both quietly burn trust I'll want later.
Memorizing CSS or a widget library does not get you here. You can look up how flex behaves. You cannot look up how to hold intent and cost in the same sentence and negotiate a landing between them.
Here's the thesis, plainly: the core skill of design engineering is negotiating fidelity. Every implemented UI is a settlement between what was designed, what's technically cheap, what's accessible, and what ships this sprint. Knowing where to land that settlement — and being able to defend the landing in both rooms — is the job.
I think about fidelity on three axes at once:
The trap for ex-frontend devs like me is optimizing visual fidelity to 100% and ignoring the other two. I once shipped a settings screen that matched the mock to the pixel and was still wrong: I'd hard-coded a hex value instead of pulling the token. Looked flawless in review. Three weeks later we shifted the palette one shade darker, every other surface followed, and that one screen sat there glowing slightly too bright until a user reported it as a "bug." Pixel-perfect and structurally bankrupt. A good design engineer trades a little visual fidelity for a lot of structural fidelity without being asked, because they already know which one the team pays for later — and it's always the structural one.
The reason this axis-thinking matters is that stakeholders only ever see the visual axis. A screenshot in a review captures visual fidelity perfectly and behavioral and structural fidelity not at all. So those two are exactly the ones that rot silently unless someone whose job it is to care about them is in the room. That someone is the design engineer.
The round-trip between Figma and production is only cheap if both ends speak the same primitives. My first real deliverable as a design engineer wasn't a feature. It was infrastructure that made the seam thinner. Three layers.
Design tokens. One source of truth for color, spacing, radius, and type, named identically in Figma and in code. When a designer says "use surface-2," there's a surface2 in my theme with the exact same value. No translation, no drift, no meeting. This is the single highest-leverage thing a design engineer can build, and it's why design tokens have become the backbone of every serious design system.
// tokens.dart — the vocabulary both rooms shareclass AppTokens { // spacing scale — designers use these same names in Figma static const double space1 = 4; static const double space2 = 8; static const double space3 = 12; static const double space4 = 16; // semantic colors, not raw hex sprinkled across 200 widgets static const Color surface1 = Color(0xFF0E0E11); static const Color surface2 = Color(0xFF17171C); static const Color textPrimary = Color(0xFFF5F5F7); static const Color textMuted = Color(0xFF9A9AA5);}Two rules make tokens actually pay off. First, name them semantically (surface2, textMuted), not by value (darkGrey, almostBlack) — because a semantic name survives a palette change and a value-name doesn't. Second, ban raw hex and raw pixel literals in feature code, ideally with a lint rule, so the tokens can't be quietly bypassed at 6pm on a Friday.
Shared components. A button in Figma and a PrimaryButton in code should have the same variants and the same names. When they diverge, every screen becomes a negotiation from scratch. When they match, most screens assemble themselves.
enum ButtonState { idle, loading, disabled }class PrimaryButton extends StatelessWidget { const PrimaryButton({ super.key, required this.label, required this.onPressed, this.state = ButtonState.idle, }); final String label; final VoidCallback? onPressed; final ButtonState state; @override Widget build(BuildContext context) { final isInteractive = state == ButtonState.idle; return FilledButton( // "disabled" always means non-interactive AND dimmed — one rule, everywhere onPressed: isInteractive ? onPressed : null, style: FilledButton.styleFrom( backgroundColor: AppTokens.surface2, disabledBackgroundColor: AppTokens.surface2.withOpacity(0.4), ), child: state == ButtonState.loading // "loading" always means: spinner replaces the label, same footprint ? const SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(label), ); }}Notice the component encodes decisions, not just styles. The loading branch keeps the same footprint as the label so the layout never jumps, and disabled is a single enum value that guarantees non-interactive-and-dimmed everywhere at once. That's the difference between a styled widget and a design-system component: the component makes the correct behavior the default and the wrong behavior hard to write.
Shared language. The cheapest fix of all: agree on words. We decided as a team that "disabled" always means non-interactive and dimmed, and "loading" always means the action is in flight with a spinner replacing the label — same width, no layout jump. Sounds trivial. It killed an entire recurring thread of "wait, which grey did you mean?"
The payoff compounds. On a recent build, once the token layer was solid, a full palette change that would've been a two-day find-and-replace nightmare took about 20 minutes and one code review. That's the whole argument for the infrastructure in one number.
The best thing I bring to a design review isn't an opinion. It's a running build.
Static mocks lie by omission. They can't show you that a beautiful multi-step form feels exhausting by the third step, or that a carousel nobody asked for adds a tap to the one thing every user does daily. You only feel those things with your thumb. So when a flow feels uncertain, I stop arguing about it and build a rough, throwaway version in an hour — real gestures, real transitions, fake data.
Half the time the prototype kills the idea faster and more kindly than any critique could. Nobody's ego is on the line; the phone just isn't fun to use, and everyone in the room can feel it at the same moment. The other half, it reveals the idea is better than the mock even suggested, and we commit hard instead of hedging.
This is why prototyping in code beats prototyping in a design tool for anything interactive. A design-tool prototype approximates behavior. A code prototype is behavior — same jank, same latency, same thumb-reach problem you're about to ship. It's the cheapest possible way to be wrong before being wrong gets expensive. Frameworks with hot reload, like Flutter, make this loop tight enough that a rough interactive prototype is genuinely faster to feel than a click-through in a design tool.
The discipline that makes this work: keep it genuinely throwaway. The moment you start caring about the prototype's code quality, you've lost the speed that made it useful in the first place. I write ugly, hard-coded, single-file prototypes on purpose, and I delete them without ceremony. If one survives, it survives as a rewrite, never as a foundation.
New design engineers get this exactly backwards. They push back on taste — where they should mostly defer — and stay quiet on systems, where they should be loud. Here's the rule I actually use.
Push back when the design breaks a system or a constraint:
| Situation | Why it's worth the conversation |
|---|---|
| A new one-off component that duplicates an existing one | It fragments the system and doubles maintenance forever |
| An interaction that fights the platform (custom back gesture, non-native scroll) | Users pay in confusion; you pay in bugs for years |
| A layout with no defined behavior for long text or small screens | It will break in production on real data, guaranteed |
| An effect with a real performance cost on a hot surface | Frames are a budget; someone has to hold the line |
| A color pairing that fails contrast for real users | Accessibility isn't optional, and it's invisible in the mock |
Just build it when it's taste and it's cheap:
The meta-skill is knowing which bucket you're in before you open your mouth. Push back on taste and you become exhausting to work with. Stay silent on systems and you're complicit in the mess you'll be maintaining next quarter. Spend your objections where they compound.
None of this is exotic. The leverage is a handful of boring habits done consistently.
/gallery route behind a debug flag. It's where I catch a broken empty state before QA does, and where a designer can poke the real thing instead of imagining it.The theme across all of it: make the expensive thing — drift between design and product — visible and cheap to catch early, so it never reaches production where it's expensive to fix and embarrassing to explain.
You can arrive at design engineering from the dev side or the design side, and each direction has one specific thing to learn.
If you're a developer (this was me): your gap is taste and design literacy. You already respect constraints; now learn to see — why a layout feels heavy, why 24px reads better than 20 here, why this particular grey is doing violence to the hierarchy. Sit in critiques and keep your mouth shut. Ask designers to explain choices you'd have skimmed past. Start caring about the states nobody drew — that instinct alone is 80% of the value you'll add.
If you're a designer: your gap is systems thinking and cost. Learn what's cheap and what's expensive to build, and why the answer is often counterintuitive. Learn to think in components and tokens instead of screens. You don't need to write production code, but you need to read it comfortably and prototype your own ideas well enough to feel them under your thumb before you hand them off.
Both paths converge on the same place: fluency in two rooms and comfort living in the seam between them. You'll know you've arrived when both designers and developers start routing the ambiguous decisions to you on purpose — not because it's in your title, but because you're the one who reliably lands them well.