Design Handoff and Developer Collaboration Questions
Getting a design built as intended: the specs, redlines and acceptance criteria a designer hands to engineers, and the communication that keeps the shipped build faithful to the design. Covers what a developer-facing spec must pin down, including component states, interaction and motion detail, breakpoint behavior, edge cases and error states, accessibility notes, and the design tokens an engineer will consume. Covers how the handoff is actually carried, in Figma Dev Mode, Storybook and the sprint ticket, and where those tools stop being enough. Also working with engineers before and during the build rather than only at the moment of handoff, resolving the tension when design intent meets technical reality, and design QA after the build: verifying the implementation against intent, visual regression checks in CI, and triaging drift without needlessly blocking a release. Both sides of the seam are examinable, including the engineer mapping design variants to component props and turning a spec into a typed component contract. Not design system and token architecture, versioning or governance; not building the prototype itself; not auditing an interface against accessibility standards.
Design an asynchronous handoff process for remote cross-functional teams across time zones. Explain how you'd structure documentation, handoff artifacts, SLAs for designer responses to dev questions, and tools to keep momentum without frequent live meetings.
Sample Answer
Direct answer
Async handoff across time zones works by moving everything a live walkthrough used to convey into a written artifact good enough that an engineer starting their day many hours later never has to wait for a live answer to make normal progress, plus a narrow, explicit response commitment for the genuine ambiguities that remain.
Structure
Documentation: one canonical spec per component or screen, not scattered across chat history, containing everything a synchronous walkthrough would have said out loud: the redline, every state, interaction notes, the tokens used, and a short "why" behind any non-obvious decision. The "why" is the first thing that gets lost when there's no live conversation to ask about it.
Handoff artifacts: a written handoff note per feature, not per file, stating what changed since the last version, what's locked versus still flexible, and linking to the acceptance criteria. Treat this note as the primary artifact, with the design file as reference detail underneath it.
Service-level agreements (SLAs, a stated commitment for how fast something will happen) for designer responses: commit to a number the team can actually hold, for example acknowledge within one business day and give a substantive answer within two, and post it somewhere engineers will actually see it, so a blocked engineer knows whether to wait or document an assumption and move on.
Tools to keep momentum: a single async question thread per feature, not scattered direct messages, so context accumulates in one place; short recorded walkthroughs (two to three minutes) for anything genuinely hard to convey in text, like a motion detail; and a visible "blocked, waiting on design" board column so stalled work is obvious without a meeting.
Worked example
A designer in a UTC+8 zone hands off a settings screen to an engineer in UTC-5, roughly a 13-hour offset with almost no overlapping working hours. The handoff note states the toggle's disabled state uses 40% opacity of the enabled color, and that this matches an existing pattern elsewhere in the product, the "why" that stops the engineer's live guess of "0.5 looks about right" from silently drifting off spec. Working while the designer is asleep, the engineer hits a real gap the spec didn't cover, what a toggle that's both disabled and in an error state should look like, and posts it to the shared question thread with a proposed default, noting they'll ship that default unless corrected inside the response window. The designer confirms it the next morning, and the engineer's next session incorporates the answer instead of the day having been blocked.
Trade-offs & pitfalls
The core failure mode of async-first handoff is under-specifying, because "we can just ask" no longer works within the hour, so documentation has to be more complete than a co-located team would ever bother writing. Skipping that completeness to save time up front just moves the cost onto blocked engineers later. A response commitment that's too aggressive, promising same-hour answers across a 13-hour gap, is a promise nobody can keep and erodes trust in every other commitment the process makes. And "propose a default and proceed unless corrected" only works if engineers document the default loudly enough for the designer to actually catch a wrong one; a silent guess with no flag recreates the exact drift the process exists to prevent.
Senior engineers ask that interactive feed components minimize layout shifts and reflows. For a rich content feed with images, ads, and inline video, describe how you would specify skeletons, image aspect ratios, lazy-loading rules, and placeholder sizing in your handoff to avoid cumulative layout shift (CLS).
Sample Answer
Direct answer
The single spec decision that prevents nearly all layout shift in a feed is reserving the final size of every unstable element, image, ad, or inline video, before its content actually arrives, so the browser never has to push other content around once real content shows up. Skeletons, lazy-loading rules, and placeholder sizing exist to make that reserved space look intentional instead of like an empty hole.
Cumulative Layout Shift (CLS) is one of the Core Web Vitals metrics: it measures how much visible content unexpectedly moves during a page's lifecycle. Each individual shift is scored as its impact fraction (how much of the viewport moved) times its distance fraction (how far it moved, relative to viewport height):
shift score=impact fraction×distance fractionSpec items to define
- Image aspect ratios: every image slot declares an intrinsic width-to-height ratio (16:9 for hero images, 1:1 for avatar thumbnails), so engineering sets width and height attributes or the CSS
aspect-ratioproperty, letting the browser reserve the space before the file even starts downloading. - Skeletons: a placeholder matching the exact final footprint of the content it stands in for, same width and height, not an approximate gray box, so its own removal doesn't cause a second shift.
- Lazy-loading rules: define which items load eagerly (anything likely above the fold on first paint) versus lazily (native
loading="lazy"on images clearly below the fold), with the boundary set as a defined pixel or viewport-height threshold rather than "whatever's not immediately visible." A vague boundary either lazy-loads something visible on a wide viewport (shifting content right where the user is looking) or eagerly loads content nobody has scrolled to yet. - Ad placeholders: reserve the exact final dimensions of one of the fixed Interactive Advertising Bureau (IAB) standard ad sizes the slot is configured for, for example 300x250, rather than "whatever size the ad turns out to be," since a third-party ad script is exactly the content a design spec cannot control after the fact.
- Inline video: reserve the player's frame at its intended aspect ratio before the video element or poster image loads, and specify that expanding controls or captions overlay that reserved frame rather than growing it.
Worked example
Take a 400px-wide, 800px-tall viewport, 320,000 square pixels of visible area. A feed item has an image slot at the top and, directly beneath it, a 200px-tall caption-and-actions block; the next item has not rendered yet.
Before the image file arrives, the unsized slot has zero height, so the caption block sits at y = 0 to 200. Once the 400x300 image loads, the slot claims its 300px and the caption block is pushed down to y = 300 to 500.
The two fractions are measured on different things, and conflating them is the most common way this calculation goes wrong. The impact fraction is about AREA: the union of the visible area the unstable element occupied in the previous frame and in the current frame, over the viewport's area. The caption block covers y = 0 to 200 before and y = 300 to 500 after, so the union spans y = 0 to 500 across the full 400px width:
impact fraction=320,000 px2400×500 px2=320,000200,000=0.625The distance fraction is about how FAR it moved, over the viewport's largest dimension, here the 800px height:
distance fraction=800 px300 px=0.375 shift score=0.625×0.375=0.234375The tempting shortcut is to take the impact area as viewport width times the shift distance, 400 x 300 = 120,000 px², giving an impact fraction of 0.375 and a score of 0.140625. That spends the same 300px twice, once as an area and once as a distance, and it understates the real score by roughly 40%. It also gets worse the more content sits below the shifting element, which in a feed is most of the page: if the caption block and the items under it had filled the viewport, the union would be the whole 320,000 px², impact fraction 1.0, and the score 0.375 from this single event.
Either way it blows past the 0.1 "good" threshold Core Web Vitals sets for a page's entire CLS score, and that is the number to put in the handoff: one unsized image in a feed can spend the whole page budget. Now specify a reserved box instead, for example aspect-ratio: 4 / 3 sized to match the image's real 400x300 footprint, so the container already occupies its final 300px of height before the file loads. The caption block never moves, the union of its before and after positions is the same rectangle, impact fraction is 0, and the shift score for that event is 0.
Trade-offs & pitfalls
Reserving space for content whose real size isn't known ahead of time, a mixed-aspect-ratio user-uploaded photo, looks like a case where the layout genuinely can't reserve space. Usually it isn't, and treating it as unsolvable in the spec is how the shift ships. The strongest version of this spec removes the guess entirely by requiring the feed API to return each item's intrinsic width and height, captured once at upload, in the payload alongside the image URL, so the client sets that item's exact ratio before the file starts downloading. That is a backend ask, not a CSS trick, which is precisely why it belongs in the handoff rather than being quietly absorbed by whoever writes the component. Where the dimensions genuinely aren't available, then the real choice is between cropping or letterboxing to a fixed ratio and accepting a small shift; name which one you're choosing, and say so in the spec rather than implying it was the only option available. Overusing skeletons for content that resolves near-instantly adds visual noise for no real benefit and can read as sluggish; scope them to content that's realistically slow to arrive.
When annotating a UI design for developers, what specific details do you include on a design screen? Provide a checklist of annotations (spacing, color tokens, interaction behavior, asset formats) and explain why each item matters for implementation fidelity.
Sample Answer
Direct answer
The annotations that most reliably prevent a build from drifting from intent fall into four categories: spacing, color (via tokens, not raw values), interaction behavior, and asset formats. A design missing any one of these still gets implemented, just not necessarily as intended, since an engineer without the annotation still has to pick something.
Spacing
I annotate the distance between every element that isn't obviously equal to its neighbors: padding inside a container, gaps in a list, margins between sections, ideally referencing a spacing scale (a small fixed set of values like 4, 8, 16, 24px, so the layout stays consistent as it's implemented across many screens by different engineers). It matters for fidelity because spacing differences of even a few pixels are invisible in isolation but compound across a page into a layout that reads as "off" without anyone being able to say exactly why.
Color, via tokens
Rather than annotating "#2F6FED", I annotate the token (a named, reusable value) it resolves to, e.g. color-primary-600. This matters for two separable reasons: it survives a later rebrand or dark-mode variant without every spec becoming stale, and it forces the annotation to reference something already validated for contrast and reuse elsewhere in the product, rather than a one-off value picked for this screen alone.
Interaction behavior
A static screen can't show what happens on hover, tap, focus, or after a delay, so I annotate it explicitly: what changes visually on each state, what triggers a transition, and roughly how it should feel (for example, "expands over about 200ms, not instantly") without pinning exact timing values better owned by an interaction spec or prototype. This matters for fidelity because interaction is often the majority of what a user actually experiences: a pixel-perfect static layout with a jarring or missing transition still fails to match intent.
Asset formats
I specify the file type for every export (SVG, a format that scales to any size without quality loss, for icons and illustrations; PNG or WebP, a compressed image format, for photographic content) and the naming convention, so engineers pull the right file without guessing which of five similarly named exports is current. Getting this wrong doesn't just cost time: a raster icon exported at the wrong density looks visibly blurry on a high-density screen, a fidelity failure a user notices immediately even if they can't name why.
Worked example
On a notification bell icon with an unread-count badge: the spacing annotation specifies 4px between the bell and badge; the badge background uses color-danger-500 (not "red"); the interaction behavior notes that the badge should scale in briefly when a new notification arrives, not appear instantly; and the bell itself is exported as SVG, so it scales cleanly at any icon size the product uses, rather than as three separate PNG density exports.
Where under-annotating costs the most
Interaction behavior is the category most often skipped, because it doesn't show up as a visible gap on the static file the way a missing spacing value does. The result is a build that's pixel-accurate at rest and noticeably off the moment a user actually touches it.
A developer reports that a pixel-perfect implementation of your layout causes layout thrashing and poor performance. How would you adjust your handoff artifacts and specifications to balance visual fidelity and runtime performance while preserving the intended UX?
Sample Answer
Framing: it's rarely fidelity itself that's expensive
Before changing anything in the spec, I'd sit down with the engineer and find out exactly which piece of the design is forcing the runtime-measurement approach that's causing the thrashing. Layout thrashing (sometimes called forced synchronous layout) happens when code repeatedly reads a layout value from the browser, like an element's rendered height, and then writes a change based on it, over and over within the same frame; each read-after-write pair forces the browser to recalculate the page's entire layout instead of doing it once, which shows up to the user as visible stutter, especially while scrolling or resizing. It's almost always caused by trying to replicate a visual relationship that depends on content measured at runtime (equal-height cards with variable content, one element sized relative to a sibling's actual rendered size) using JavaScript, rather than a fixed value CSS (the browser's styling language) can express directly.
Diagnosing which specific requirement is driving it
I'd ask the engineer to point to the exact visual behavior that needed measurement: common culprits are forcing multiple independently rendered cards to always match the tallest one's height, centering an element relative to a sibling whose height isn't known until content loads, or scaling one element as a ratio of another's measured size. Once I know which specific relationship is expensive, I can address that one thing instead of loosening fidelity across the whole screen.
Adjusting the handoff spec
- Redesign the spec around what modern CSS can express natively. Most "pixel-perfect measured" effects (equal-height cards, centering, proportional sizing) have a CSS-native equivalent, CSS Grid or Flexbox's
align-items: stretch, for instance, that the browser computes once during its normal layout pass instead of forcing an extra JavaScript-driven recalculation. I'd revise the spec to describe the outcome ("cards in a row must appear equal height") and explicitly leave the technique to the engineer, rather than specifying a pixel value that implies a measurement step. - Replace zero-tolerance pixel values with an explicit tolerance for anything driven by dynamic content: "vertically centered within 2px" instead of "must be pixel-identical." A 2px variance is invisible to a user; chasing exact equality is frequently the actual source of the expensive measurement loop.
- Mark, directly in the spec, which values are structural and fixed (safe to hardcode exactly: the spacing scale, breakpoints, fixed-size icons) versus which are content-dependent and should carry a tolerance or an explicitly CSS-native technique instead of an exact target.
- Walk the engineer through which visual guarantee is load-bearing for the experience versus incidental to how the original mockup happened to look, given the specific sample content used when I designed it.
Preserving the intended UX
The output of that conversation should be a named, non-negotiable outcome (for example, "cards must never render at visibly different heights"), decoupled from the specific implementation that was originally driving cost. If that outcome can be reached with a native CSS technique and zero runtime measurement, the user-facing experience is fully preserved and the performance problem disappears at the same time. If it cannot be reached natively, the conversation becomes an explicit, priced trade-off rather than a silent one: I rank the visual guarantees so the team knows which single relationship is worth paying a measurement pass for, and which ones I will loosen to a tolerance to buy it. What I never do is leave a spec in place that implies a measurement step nobody agreed to pay for, because that is how the cost gets rediscovered by a user on a slow device instead of by us in review.
Worked example
Take a three-column "PricingCard" component, each card holding a variable-length bullet list of plan features. The original build measured every card's rendered height with getBoundingClientRect() (the DOM method that returns an element's current on-screen size and position) after content painted, took the tallest value, and then wrote that height back onto the other two cards' inline style.height, redoing this on every resize and every time card content changed. Because writing style.height on one card invalidates the very layout the next card's getBoundingClientRect() read depends on, the browser was forced to recalculate layout on every iteration of that loop, once per card, which is exactly the read-write-read-write pattern that causes thrashing.
I'd revise the spec to describe only the outcome: "all three cards in a row must render at the same height as their tallest sibling, regardless of content length," and drop any implied measurement step from the spec entirely. The engineer replaces the manual measuring loop with a single CSS rule: setting the card row to display: grid; grid-template-columns: repeat(3, 1fr); with align-items: stretch (CSS Grid's default), which makes every card in a row stretch to match the tallest one automatically, computed once during the browser's normal layout pass with zero JavaScript and zero measurement. The visual result is identical to what the manual height-matching produced; the difference is entirely in how expensive it was to produce.
Trade-offs and pitfalls
One failure mode is holding the line on the original pixel-exact spec without understanding what's actually expensive about it, which forces the team into either an unnecessary rebuild or, worse, shipping the jank because nobody wants to escalate. The opposite failure mode is over-correcting: loosening tolerance across the whole screen instead of just the specific dynamic relationship that was measured, which quietly degrades quality everywhere performance wasn't actually the problem. The fix has to be scoped to the specific interaction that was expensive, not a blanket retreat from fidelity.
You're building a button for the shared library with primary, secondary, and destructive styles, each needing default, hover, and disabled states. Would you model these as separate Figma components or as variants of one component, and why? How does that choice change what a developer has to do to map it to code props?
Sample Answer
Direct answer
Model it as one button component with two variant properties, style (primary, secondary, destructive) and state (default, hover, disabled), rather than three separate components. Those are really the same interactive element with different treatments, and a single component with variant properties maps directly onto a single code component with a small, typed set of props, while three separate components would map onto three near-duplicate code components, or force the developer to notice they were always meant to be one.
Structured elaboration
A variant, in the design-tool sense, is a grouped version of one component that shares structure and behavior and differs only along a defined property, functionally close to how a code component exposes props or enum values.
Reasons to model this as one component with variants:
- A single interaction model to maintain: hover and disabled behavior are defined once, not copy-pasted three times and left to drift apart.
- Two independent properties, style and state, multiply combinatorially: three styles times three states is nine combinations. Variants handle that as two properties instead of nine separately named, hand-maintained layers.
- It matches how a component-based frontend already thinks: one button component taking a variant and a disabled flag, not three unrelated components imported separately.
Separate components only make sense if the styles diverge in more than color and behavior, for example if the destructive style needs a structurally different layout, like an inline confirmation step, enough that treating it as the same underlying thing would be misleading.
Worked example
On the design side: one button component with a style property (primary, secondary, destructive) and a state property (default, hover, disabled), giving nine named combinations from a single definition.
On the code side, that maps directly onto a single typed component:
type ButtonProps = {
variant: 'primary' | 'secondary' | 'destructive';
disabled?: boolean;
children: React.ReactNode;
};
function Button({ variant, disabled, children }: ButtonProps) {
return (
<button className={`btn btn--${variant}`} disabled={disabled}>
{children}
</button>
);
}
In plain language, that code says: the Button component takes three named inputs, called props. The first, variant, is restricted to exactly the three values 'primary', 'secondary', or 'destructive', the same three values as the style property in Figma, so a developer can't accidentally pass in a fourth, unapproved style; that restriction to a fixed list of allowed values is what "union type" means. The ? after disabled marks it as optional: a button that doesn't specify it just defaults to not-disabled, mirroring how the default state in Figma is the one you get without picking anything else. children is whatever content sits inside the button, normally its label text, typed as React.ReactNode, a built-in type meaning "anything React can render," so the same prop also covers a label plus an icon without needing a separate prop for each. The function underneath just reads those three props back out and uses variant to choose a CSS class name (btn--primary, btn--secondary, or btn--destructive) and uses disabled to set the native disabled attribute. The practical result: one Figma property change, like adding a fourth style, becomes one new value added to that variant list and one new CSS class, not a new component for the developer to build and wire up separately.
Hover doesn't need its own prop here, since it's a real pointer interaction handled by a CSS hover rule tied to the same class, mirroring how hover is a state on the design component rather than something a developer has to wire up separately. Disabled does need an explicit prop, since it changes real behavior (whether clicks register), not just appearance.
Contrast that with the separate-components version: three components, each duplicating the same disabled logic and hover styling with copy-pasted styles. A developer maintaining that has to remember to update all three when, say, the disabled opacity value changes, and it's easy to miss one.
Trade-offs and pitfalls
Overusing variants for things that aren't really style differences, cramming genuinely different layouts into one component's variants, produces a component with confusing conditional internals that's harder to reason about than three honest separate ones would have been. A variant property with far too many values on one axis is itself a sign the underlying design should be split, not merged into one component. And the developer still needs the states named explicitly as a prop or a native browser state, not just visually implied on the design canvas; the variant structure only removes ambiguity when it's paired with a note on which states are pointer-only, like hover, versus real props, like disabled.
Unlock Full Question Bank
Get access to all Design Handoff and Developer Collaboration interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.