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.
During handoff you discover the engineering team uses a different naming scheme for color and spacing tokens than your design system. How would you reconcile these differences and align both teams to a shared token map while minimizing disruption to ongoing implementation?
Sample Answer
Direct answer
I'd reconcile the naming mismatch by building a shared token map, a single side-by-side reference both teams treat as the source of truth, in two layers: a short-term alias so the two token sets resolve to the same underlying values without anyone renaming anything in code they're actively working on, and a longer-term migration plan to a single canonical name once the disruption cost of renaming is actually low. Trying to force an instant rename across an in-flight codebase is the mistake that causes the most disruption, not the one that prevents it.
Step 1: audit and categorize
I export the design system's token list from Figma (name, value, and where it's used) and pull engineering's existing token file, then build a side-by-side mapping. Each pair falls into one of three buckets: exact matches (same value, different name, like neutral-100 versus gray-10), semantic near-matches (close but not identical values that need a decision about which one becomes canonical), and tokens that exist in only one system and need to be added to the other.
Step 2: short-term aliasing
For exact matches and semantic equivalents, I propose adding aliases in engineering's token layer so both names resolve to the same value. This is the part that "minimizes disruption to ongoing implementation": nobody has to change any code that already references gray-10, because it now quietly points to the same value as neutral-100.
Step 3: migration plan and governance
I prioritize renaming by usage: the tokens referenced in the most places get the most careful phased rollout, less-used tokens can rename sooner. I also establish a single token owner (a cross-functional role, not one team unilaterally deciding names) so the next naming mismatch gets caught before ten components have shipped with it.
Step 4: communicate and verify
A short working session with both teams to walk through the mapping and agree on the canonical direction, then updating any shared component library (like Storybook, a tool that renders each component in isolation for review) to reflect the aliases, followed by a quick regression pass to confirm nothing visually shifted. Make that pass concrete rather than a look around the app: build the tokens and diff the compiled output (the generated CSS or platform files) against the previous build. Apart from the newly added tokens it should come out byte-identical, because an alias layer that changes any existing computed value is not an alias layer, it is a silent restyle wearing one, and a diff of the compiled artifacts is the only cheap way to prove which of the two you just shipped.
Worked example
Suppose the audit turns up this mapping:
| Design token | Code token | Value | Category |
|---|---|---|---|
| neutral-100 | gray-10 | #F5F5F5 | exact match, different name |
| primary-500 | brand-blue | #0B74FF | exact match, different name |
| spacing-2 | space-sm | 8px | exact match, different name |
| spacing-3 | (missing) | 16px | design-only, needs adding |
The alias layer in the engineering token file would look like this, so existing code referencing gray-10 keeps working unchanged while the design-facing name becomes the documented canonical one:
{
"color": {
"neutral-100": { "value": "#F5F5F5" },
"primary-500": { "value": "#0B74FF" },
"gray-10": { "value": "{color.neutral-100.value}" },
"brand-blue": { "value": "{color.primary-500.value}" }
},
"spacing": {
"spacing-2": { "value": "8px" },
"spacing-3": { "value": "16px" },
"space-sm": { "value": "{spacing.spacing-2.value}" }
}
}
The shape of that file is the part to get right, and it is easy to get wrong in a way that looks correct. The canonical token is defined once with a literal value, and the legacy name is a reference to it, not a second copy of the same value. The curly-brace syntax is the reference form common to token build tools (Style Dictionary and the W3C design-tokens draft both use this shape, and your pipeline may spell it slightly differently); what matters is that the build resolves a pointer rather than trusting two hardcoded values to stay equal forever. Write gray-10 instead with its own "value": "#F5F5F5" and a note saying it corresponds to neutral-100, and the file reads as aliased while behaving as a duplicate: the first time the canonical grey is adjusted, neutral-100 moves and gray-10 silently does not, and a naming problem has quietly become a colour-drift bug on every screen still using the old name. A real reference also fails loudly at build time if the canonical name is misspelled or removed, where a copied literal fails silently, visually, and months later.
spacing-3 is defined directly rather than as an alias, since it doesn't exist in the code token file yet and so has nothing to point at. The team agrees neutral-100, primary-500, and spacing-2 are the canonical names going forward, and new code is asked to reference those directly, while old references keep working through the alias until a low-disruption window (like a planned refactor) is used to update them.
Trade-offs and pitfalls
Aliasing forever avoids disruption but means two names permanently exist for one value, which is its own long-term confusion if nobody ever completes the migration; it needs a real deadline, not an open-ended one. The pitfall to avoid is skipping the audit step and reconciling names from memory or a quick conversation: values that look equivalent (two similar grays) are sometimes intentionally different, and merging them silently introduces a visual regression instead of fixing a naming problem.
How would you annotate an interactive prototype for a multi-step checkout flow so developers clearly understand micro-interactions, validation and error states, network failure handling and edge cases? Provide a suggested annotation structure and examples for validating coupon entry and failed payment flows.
Sample Answer
Direct answer
Annotating an interactive prototype for a complex flow means adding a structured, numbered layer on top of the clickable frames that documents exactly what a click-through cannot show on its own: validation rules, error conditions, network-failure handling, and what happens on failure, tied to specific points in the flow with consistent reference markers. That way an engineer building from the prototype has one place to look up "what should happen here" instead of guessing from the happy-path click-through alone.
Structured elaboration
A suggested annotation structure:
- Numbered markers placed directly on the frame, each keyed to a numbered entry in a linked specification, rather than long paragraphs cluttering the frame itself.
- Each entry follows a consistent format: Trigger (what causes this), Behavior (what happens, including timing), Validation rule (if applicable), and Failure or edge case (what happens if it goes wrong).
- A separate frame, or frame variant, for each distinct state the click-through cannot represent inline: a loading frame, an error frame, an empty or edge-case frame, each linked from the relevant point in the flow rather than only showing the success path.
- A short legend on a cover frame at the start of the prototype explaining the annotation convention itself, so a first-time reader is not left guessing what the numbered markers mean.
Worked example
Coupon entry. Marker at the coupon input field: Trigger, the user clicks Apply with a code entered. Behavior, the field shows an inline loading indicator while validating, with no fixed duration specified since it is tied to actual response time. Validation rule, the code must match an active, unexpired coupon. Failure or edge case: an invalid code shows "This code is not valid," an expired code shows "This code has expired," and both keep the entered text in the field rather than clearing it, so the user can see and correct what they typed. This links to a separate "coupon error state" frame.
Failed payment. Marker at the "Place order" button: Trigger, the user clicks with a completed payment form. Behavior, the button shows a loading state and disables itself to prevent a double submission, itself a required behavior worth annotating, not an assumption. Validation rule, payment must be authorized by the payment processor before proceeding. Two distinct failure paths need separate annotation here, since they are not the same problem: a declined card returns the user to the payment step, shows an inline message naming the likely cause without exposing raw processor error codes, and preserves every other field already filled in, so a decline does not cost re-entering the whole form; a network failure or request timeout (the request never completing within a defined time limit) shows a different message telling the user to check their connection and try again, with a retry action, rather than reusing the declined-card copy, since the user needs to know whether the problem is their card or their connection. Both link to their own separate frames.
Trade-offs and pitfalls
Annotating every possible micro-detail on every frame makes the prototype unreadable; reserve numbered markers for decisions that are not obvious from the visual alone. A loading spinner appearing is obvious from a frame; whether a double submission is actually prevented is not. A prototype's click-through can also create a false sense of completeness, since a reviewer clicking only the happy path may never discover that error and edge-case frames even exist; the cover-frame legend and a listed summary of covered states make sure they actually get found, not just built. And exposing raw error copy is a common miss when annotation focuses only on "what happens" and skips "what does the user actually see"; always annotate the exact user-facing copy, not just the category of behavior.
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.
Describe best practices for exporting and optimizing SVG icons for the web so they can be used inline and as files. Include details on ID/metadata stripping, viewBox handling, accessibility (title/desc), and when to use symbol/sprite vs individual files.
Sample Answer
Direct answer
A clean Scalable Vector Graphics (SVG) export for the web does three things a raw design-tool export usually doesn't: it strips the tool's own generated clutter so the file is small and its internal names don't collide with anything else on the page, it sets the viewBox correctly so the icon scales cleanly at any size instead of being locked to its export dimensions, and it exposes an accessible name for anything that carries meaning rather than being purely decorative.
ID and metadata stripping
Design tools (Figma, Sketch, Illustrator) export SVGs full of tool-specific clutter: auto-generated identifiers like path-3-inside-1_2043_881, a <title> tag set to the design layer's internal name, editor namespaces, and unused <defs> blocks. Running the export through a cleanup tool such as SVGO, a widely used SVG optimizer, strips this safely, but two things must never be stripped blindly: an identifier actually referenced by a <use> element elsewhere, and any accessibility markup added deliberately. A designer handing off SVGs should flag which identifiers are load-bearing rather than letting an automated pass remove all of them.
viewBox handling
The viewBox attribute (four numbers: min-x, min-y, width, height, defining the SVG's internal coordinate system) should match the icon's actual drawn bounds, and the SVG's own width/height attributes should either be omitted or match the viewBox's aspect ratio. A viewBox that doesn't match the visible artwork, common when an export includes extra invisible canvas padding from the design tool, renders the icon off-center or with unwanted whitespace once it's resized by CSS.
Accessibility: title and desc
A meaningful icon, one that conveys information on its own, like a standalone delete-icon button with no visible text label, needs a <title> element inside the SVG plus role="img". Two different specifications are doing the work there, and mixing them up in a handoff note is a common source of confusion. <title> and <desc> are elements defined by the SVG specification itself, exactly like <path> or <g>: <title> supplies the graphic's accessible name, and <desc> adds a longer description for a genuinely complex illustration. role="img" and aria-hidden="true" come from Accessible Rich Internet Applications (ARIA), a separate standard for exposing UI semantics to assistive technology. They are needed together rather than interchangeably: the SVG element supplies the text, and role="img" is what tells assistive technology to treat the whole SVG as one graphic and announce that text instead of walking into its individual shapes.
Because support for reading a bare <title> has been inconsistent across screen readers, the durable form gives the <title> an id and points at it with aria-labelledby, so the name is exposed through the ARIA mechanism as well as the SVG one. That id has to be unique on the page, which collides directly with inlining the same icon many times, so specify that the identifier is generated per instance rather than baked into the exported file. A purely decorative icon, one that always sits beside its own text label, like a small icon inside a button that already says "Save," needs none of this and should instead be marked aria-hidden="true" so a screen reader skips it entirely rather than announcing something redundant.
Symbol/sprite vs individual files
A <symbol>-based SVG sprite, one file defining many icons by identifier, each referenced elsewhere via <use href="#icon-name">, pays off once a product uses enough repeated icons that the byte savings and single-request benefit outweigh the added build complexity, typically past a couple dozen distinct icons reused across many pages. Individual files, or inline SVG per component, are simpler to reason about, let one icon change independently, and make more sense for icons used sparingly or that need per-instance fill colors a shared sprite entry can make trickier to override.
Worked example
A rating-star icon exported raw from a design tool: <svg width="24" height="24" viewBox="0 0 24.5 24.3"><title>Layer 3 Copy 2</title><defs><clipPath id="clip-path-928-unused">...</clipPath></defs><path id="path-3-inside-1_2043_881" d="M12 2l..." fill="#F5A623"/></svg>. Cleaned up for use as a standalone meaningful icon, an average-rating badge with no adjacent text:
<svg width="24" height="24" viewBox="0 0 24 24" role="img" aria-labelledby="rating-star-title">
<title id="rating-star-title">Rated 4.5 out of 5 stars</title>
<path d="M12 2l..." fill="#F5A623"/>
</svg>
The tool-generated id (path-3-inside-1_2043_881) is gone, but a deliberate one (rating-star-title) has been added, which is the distinction the stripping section is about: the cleanup pass removes identifiers nothing references and keeps the ones something does. If this badge renders once per row in a list, that id has to be generated per instance, or every duplicate after the first is invalid markup and the accessible name resolves to the wrong element.
If the same star instead sits inside a "4.5 (128 reviews)" text row, it's decorative, since the adjacent text already conveys the information:
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2l..." fill="#F5A623"/>
</svg>
Trade-offs & pitfalls
Stripping metadata too aggressively with a default optimizer configuration can delete an identifier a <use> reference or animation depends on, silently breaking the icon rather than just shrinking the file; always test optimized output, never trust it blind. Sprites add indirection, an icon's real markup lives in a shared file, not next to where it's used, that can slow a small team down more than it saves; don't adopt a sprite system before the icon count actually justifies it.
You must hand off an interaction that relies on complex backend behavior (optimistic updates, real-time syncing, and conflict resolution). Explain how you would document API expectations, sequence diagrams, edge cases, and UX fallbacks so frontend and backend teams can implement reliably and testably.
Sample Answer
The spec that matters most here isn't the visual mockup, it's the sequence of what the client shows the user at each point in a multi-step exchange with the server, because the UI has to represent server states the user can't directly see (pending, confirmed, conflicted, failed). Document that as an explicit sequence diagram and a state table, not prose alone, so frontend, backend, and QA (quality assurance, the team verifying the build before release) read the same source instead of interpreting a paragraph three different ways.
Define the three concepts being documented
- Optimistic update: the client updates the UI immediately, assuming the server request will succeed, instead of waiting for a response before showing the change. This makes the app feel instant, at the cost of sometimes being wrong and needing to correct itself afterward.
- Real-time syncing: changes made by one client, or by the server, are pushed live to other connected clients over a persistent connection (commonly WebSockets, a connection that stays open rather than each client repeatedly asking "did anything change?").
- Conflict resolution: what happens when two updates to the same data arrive close together and can't both simply apply, for example two users reordering the same list at once. The system needs a rule for which update wins, and a way for the other one to find out.
Document API expectations explicitly
For each backend-touching action, specify the endpoint and payload contract, the fields returned on success, and every non-success response: a validation error, a conflict response (commonly a 409 status code, the standard signal that a competing update already happened), a server error (a 5xx status code), and a timeout. Also state a rough acceptable latency band for the interaction to still feel instant; if the real round trip regularly takes longer than that, the optimistic pattern alone doesn't cover it and a stronger loading affordance is needed instead.
Sequence diagram
A sequence diagram makes the timing and branches explicit in a way prose can't: who does what, in what order, and what each of the possible endings (success, conflict, network failure) looks like.
sequenceDiagram
participant U as User
participant C as Client (optimistic)
participant S as Server
participant D as Data store
U->>C: Reorder item in list
C->>C: Apply update locally, render new state
C->>S: PATCH /items/{id} order=3 (clientVersion=7)
activate S
alt server accepts (version matches)
S->>D: Write new order
D-->>S: OK, version=8
S-->>C: 200 {version: 8}
C->>C: Reconcile: confirm local state, clear pending flag
else version conflict (another client already wrote version 8)
S-->>C: 409 {serverVersion: 8, serverState: {...}}
C->>C: Roll back optimistic change
C->>U: Show "someone else updated this, review changes" banner
else network failure
C->>C: Keep change queued, mark item as "syncing"
C->>S: Retry with backoff
end
deactivate S
The diagram's conflict branch relies on a specific technique for detecting conflicts: each record carries a version number that increments on every successful write, and the client sends the version it started from along with its edit (clientVersion=7). The server compares that number to its own current version before applying the write, an approach called optimistic concurrency control, or version stamping. If the numbers match, the server applies the change and increments the version to 8. If they don't match, as when another client already wrote and bumped the version to 8 first, the server knows this client was editing stale data and rejects it with a 409 instead of silently overwriting the newer change. That version comparison is what "conflict resolution" from the definition above actually looks like in a concrete implementation: comparing version numbers, rather than, say, comparing timestamps or full document contents.
Edge cases to enumerate explicitly
- Two users edit the same item concurrently (the core conflict case).
- The optimistic update applies locally but the network request never completes because the device goes offline mid-action.
- The server accepts the write, but the real-time push that would confirm it back to the originating client is delayed or lost.
- A user takes a second action before the first one's server response has returned (stacked optimistic updates).
- A client reconnects after being offline with a queue of unsent optimistic actions.
UX fallbacks for each failure mode
- A visually distinct "pending" state on just the affected element (a subtle dimmed style or small sync icon), not a page-level loading state, since the rest of the UI is already correct.
- A non-blocking notification for conflicts with a clear resolution action, for example "Someone else updated this. Keep your change or use theirs?" rather than silently overwriting one side.
- Automatic rollback with a brief, undo-able notification if a request ultimately fails, rather than leaving the UI in a state the server never confirmed.
- A distinct "reconnecting" indicator, separate from a conflict notice, so users don't confuse temporary network loss with an actual data conflict.
Worked example
For the reorder interaction diagrammed above, the state table an engineer would implement against:
| Client state | Trigger | Visible UI |
|---|---|---|
| Pending | User reorders an item | Item dims slightly; small sync icon on the drag handle |
| Confirmed | 200 response with matching version | Dim state clears; no other visible change |
| Conflicted | 409 response, server has a newer version | Banner: "Someone else updated this list. Keep mine / Use theirs" |
| Failed | Timeout or repeated 5xx after retries | Item reverts to its prior position; toast: "Couldn't save your change. Retry" |
Trade-offs and pitfalls
- Specifying only the success path is the most common gap in handoffs like this. The happy path barely needs a diagram at all; the value of this documentation is entirely in the failure branches.
- A single generic error message for every failure mode (conflict, network failure, validation) leaves the user unable to tell what happened or what to do about it. Distinguish them in the design.
- Rolling back silently, with no visible notification, makes the app feel less trustworthy than a slower, non-optimistic update would have. Taking on optimistic updates means taking on the obligation to visibly handle their rollback.
- This pattern adds real engineering complexity, including idempotency handling (a way to mark a retried request as "the same one," so a retry doesn't accidentally apply a change twice). Reserve full optimistic-update treatment for interactions where responsiveness genuinely matters, not every mutation in the product.
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.