"Just hand it off to Dev Mode" treats handoff as a file format problem — get the right numbers out of Figma and into CSS. Numbers are the easy 80%. Auto Layout was built with a direct CSS analogue in mind, so its properties translate almost mechanically. The other 20% is everything Auto Layout was never asked to represent: what happens when a name is forty characters instead of ten, what the button looks like focused instead of hovered, which breakpoint this frame collapses at, why the gap is 12px here and 16px two frames over. None of that is hidden from Dev Mode on purpose — it simply isn't data the design tool was ever tracking, so there's nothing for an inspector panel to surface.
Treating handoff as translation instead of export changes what "done" means. A translator doesn't just swap words for their dictionary equivalents; they decide what the original was actually trying to say and find the target language's own way of saying it. A developer doing the same thing with a Figma frame isn't failing to use Dev Mode correctly if they diverge from the exported snippet — they're doing the part of the job Dev Mode was never going to do for them.
01A translation, not an export
Dev Mode's inspector is genuinely accurate about one thing: the state of the frame exactly as designed, exactly as sized, exactly as populated. That's a narrower claim than it sounds like. A card component designed with a two-line heading and a 40-character description is a specification for that heading and that description — not a guarantee about what a three-line heading or a 200-character description will do to the layout, because the design frame doesn't hold those states to measure. The exported CSS is correct and, on its own, incomplete; it describes a single frozen instant of a component that in production will run through dozens of instants a design frame never modeled.
The tell that handoff is being treated as export: a developer who copies Dev Mode's CSS variable-for-variable and ships it without testing what happens when the real API response is longer, shorter, or empty. The code will match the frame perfectly and still break the first time a user with a long name logs in.
02How Dev Mode maps Auto Layout to Flexbox
This part really is close to mechanical, because Auto Layout's properties were modeled on Flexbox in the first place. Dev Mode's inspector panel and its exported CSS both follow the same property-for-property mapping:
| Auto Layout property | Flexbox equivalent | Notes |
|---|---|---|
| Direction (vertical / horizontal) | flex-direction: column / row |
Wrap adds flex-wrap: wrap |
| Gap | gap |
A single value unless "Independent spacing" is on, which splits it into per-child margins |
| Padding | padding |
Per-side values map 1:1 |
| Primary axis alignment | justify-content |
Packed vs. Space Between changes the keyword used |
| Counter axis alignment | align-items |
Per-child override becomes align-self |
| Resizing: Hug contents | width / height: fit-content |
See the resizing section below for the real behavior |
| Resizing: Fill container | flex: 1 1 0% |
Requires the parent to itself be a flex container |
| Resizing: Fixed | width / height: <value>px |
The one mapping that carries no intent, only a number |
That last row is worth sitting with. "Fixed" in Figma can mean two very different things that produce identical CSS: this value must never change (a 24px icon), or nobody has decided what this should do yet (a designer picked a width that looked right for the one string they tested with). Dev Mode exports both as the same hardcoded pixel value, because the distinction between "constrained on purpose" and "not yet resolved" was never a property Figma had a field for. A developer reading only the exported number has no way to tell which one they're looking at — which is exactly why this is a judgment call and not a lookup.
03When the honest target is Grid, not Flexbox
Dev Mode always exports Auto Layout as flex properties, because
that's the CSS model Auto Layout was built to mirror — it has no
concept of a grid frame and never emits display: grid
on its own. That's a reasonable default for the single-axis stacks
Auto Layout handles well, and a poor fit the moment a layout is
genuinely two-dimensional: a card grid that needs to reflow column
count by available width, a form where labels and inputs must
align across rows, a dashboard of tiles with mixed spans. Frames
built with nested Auto Layout containers can simulate a
grid, but the export still comes back as nested flex, and a
developer who implements it that way inherits every alignment bug
Grid exists to solve — labels drifting out of column, gutters that
don't stay even when one cell wraps to a second line.
The judgment call here isn't "ignore what Dev Mode exported" —
it's recognizing the pattern the nested frames were standing in
for, and reaching for display: grid with
grid-template-columns when the shape is truly a grid,
rather than replicating the nested-flex structure the design tool
was forced into because it had no native alternative.
04Hug, Fill, and Fixed under real content
Resizing modes are where the single-state-frame problem shows up most directly, because each mode encodes an assumption about content that only holds for the content the designer happened to type in.
- Hug contents assumes the container should always be exactly as big as what's inside it. That's correct for a badge or a button label. It's a bug waiting to happen on a card in a grid, where every sibling needs to hug its own content and the row ends up with mismatched heights the moment descriptions run to different lengths — a layout that looked even in Figma because every test card happened to have a two-line description.
-
Fill container only resolves relative to a
parent that is itself flexible. Dev Mode will happily export
flex: 1for a child whose parent has a fixed pixel width, and the code will compile and look identical to the design — right up until the parent's width actually needs to change, at which point Fill was never really "filling" anything. - Fixed is accurate until real data doesn't fit it. A fixed-width name field sized to "Alexandra Petrov" will truncate or overflow the first name forty characters long — a state Figma never rendered because nobody typed a forty-character name into the mock.
None of these are Dev Mode reporting the wrong value. Each one is reporting the right value for a frame that, by construction, only ever holds one version of the content. Reproducing the resizing mode faithfully is necessary; treating it as sufficient is the mistake.
05What Dev Mode structurally cannot see
Some gaps aren't oversights in a particular file — they're outside what a static, single-state design frame is capable of encoding in the first place, no matter how carefully it was built.
| Category | Why the frame can't hold it |
|---|---|
| Semantic HTML |
A layer named "Button" exports as a positioned
<div>, not a <button>
— Figma has no concept of an element's semantic role,
only its visual layer type
|
| Interaction states | Focus, active, disabled, and error states exist only if someone built a separate frame or variant for each one; most files stop at hover |
| Other viewports | A frame is one fixed width; how it reflows at every size between mobile and desktop is a decision the frame was never asked to make |
| Real / variable content | Empty states, loading states, error messages, and long-tail content lengths aren't in the mock unless someone explicitly designed them |
| Accessible names and roles | ARIA attributes, alt text, and landmark structure have no Figma equivalent to export at all |
| Intent behind a value | Dev Mode reports that padding is 24px; it can't say whether that's a deliberate token or a value someone nudged once and forgot to reconcile |
None of this is a criticism of Dev Mode doing its job badly — a design tool that only ever renders static frames has no channel to carry information that only exists in motion, in an error condition, or in a screen reader's accessibility tree. The practical implication is that "Dev Mode coverage" and "handoff complete" are different milestones, and treating the first as the second is where specs quietly go stale.
06Variants and props: a lossy translation both ways
Figma's component properties — boolean, variant, instance-swap,
text — map conceptually to a code component's props, but the
translation loses information moving in either direction. A
variant set organizes states as a flat grid of named combinations
(size=large, state=hover); a well-built code
component usually wants those same axes as independent typed
props (size, state) so invalid
combinations can't be constructed. Reproducing the variant grid
literally — a giant switch statement keyed to variant names —
tends to reintroduce the exact copy-paste drift variants were
supposed to eliminate on the design side, just relocated into
code.
The reverse gap matters too: nested instance overrides deep inside a variant frequently don't survive Dev Mode's export cleanly, and a developer working strictly from the generated snippet can miss an override that was clearly visible — and clearly intentional — in the design file itself. Reading the component structure in Figma directly, not just its Dev Mode output, is often the only way to catch that before it ships as a silent regression.
07Annotation habits that prevent drift
Everything above is a one-time translation problem. Drift is what happens after that first handoff, once the design file and the codebase start evolving independently and nothing keeps them honest about which one is current. A handful of habits catch most of it before it reaches production:
- State the intent, not just the value. A Dev Mode comment or annotation layer that says "24px — spacing/xl token, not a one-off" turns an ambiguous fixed number into a traceable decision a future editor won't second-guess or quietly change.
- Design the states you expect in production, or say explicitly which ones weren't designed. A component missing an error or empty state should say so on the frame — "no error state designed, follow the pattern in [related component]" — rather than leave a developer guessing whether the omission was deliberate.
- Gate on a status, not a stage. A "Ready for dev" label on the frame itself, kept current as the design changes, is a clearer signal than a Slack message that ages out of everyone's memory within a week.
- Link the frame to the code, not just to a ticket. A comment pointing at the actual component file or PR lets anyone jump from spec to implementation and back, instead of relying on two people's memory of which commit shipped which design.
- Version the frame when the design changes post-handoff. A duplicated "v2" frame with a dated note on what changed is worth more than editing the original in place and leaving developers to notice the diff on their own — or not notice it at all.
- Close the loop after implementation. A developer who had to make a judgment call Dev Mode couldn't resolve — which resizing mode was really intended, what an empty state should look like — should be able to report that decision back onto the frame, so the next person reads the resolved answer instead of hitting the same ambiguity cold.
A cheap test for drift risk: open a component that shipped more than a few months ago and check whether the Figma frame still matches production. If it doesn't, and nothing on the frame explains why, that's the annotation habit that was skipped — not a one-off mistake.
08A worked example
A card component, Auto Layout vertical, Hug contents on height, Fill container on width, 16px gap, 24px padding, nested inside a horizontal Auto Layout row with Space Between:
Dev Mode's export for the card is accurate and directly usable:
.card {
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px;
flex: 1 1 0%; /* Fill container, relative to the row */
}
.card-row {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 16px;
}
That CSS is correct and also silent about the one decision that
actually matters here: should mismatched card heights in a row be
acceptable, or does this pattern need align-items: stretch
on the row plus an internal strategy — a line-clamp on the
description, a fixed-height content area — to keep the row
visually even once real content stops matching the mock? Dev Mode
has no opinion, because the design frame never modeled a mismatch
to have an opinion about. That's the judgment call, and it's a
five-minute conversation with design that's far cheaper before
the row ships than after someone screenshots the uneven grid in
production.
09Cheat sheet
- Auto Layout maps to Flexbox almost mechanically — direction, gap, padding, and alignment translate 1:1. Treat that mapping as reliable; it's the part Dev Mode was built to get right.
-
Dev Mode never emits
display: gridon its own. Nested Auto Layout simulating a two-dimensional grid is a sign to reach for real CSS Grid, not to replicate the nested-flex structure literally. - A "Fixed" resizing value carries no signal about whether it was a deliberate constraint or an untested guess — that distinction has to come from a conversation or an annotation, never from the number alone.
- Hug and Fill are only as safe as the content that was in the frame when they were set. Test every resizing decision against the longest and shortest real content it will actually see.
-
Semantic HTML, ARIA roles, and accessible names have no Figma
equivalent. A layer named "Button" is still a
<div>until a developer decides otherwise. - A missing state — error, empty, focus — usually means nobody designed it, not that it doesn't need to exist. Confirm which one it is before shipping a guess.
- Read nested component overrides in the Figma file directly for anything with deep variant nesting; Dev Mode's flat export can miss an override that's clearly visible in the layers panel.
- Annotate intent, not just values — "token, not a one-off" on a padding number saves the next editor from re-litigating a decision that was already made.
- When a design changes after handoff, version the frame instead of editing it silently in place. A dated "v2" with a note on what changed is what actually stops drift.