Every "Figma to React" article I've read describes the first 20% of the job.
Export the tokens. Set up a plugin. Generate the component scaffold. Ship a blog post about how AI closed the design-to-code gap.
None of that is wrong. It's just not the part that takes time. The part that takes time is the same regardless of tooling: turning a static screen into a component that survives real content, real states, and real screen sizes. Here's the workflow I actually use, after doing this professionally — as both the designer and the engineer — across three very different products.
Start With Structure, Not Pixels
Before I open VS Code, I ask one question in Figma: what is this, structurally? Not "what does it look like" — I can see that. What is it made of.
A card isn't a 340×180 rectangle. It's a container with an image slot, a title that can wrap to two lines and then truncate, a description that can be empty, and a footer that holds one to three actions depending on permissions. If I design it as a fixed frame and hand it off as a fixed frame, I've handed off a picture, not a component.
So the first pass in Figma is always: turn it into real auto layout, with real components and variants for every state I can think of — empty, loading, one line, three lines, long title, no image. If a state doesn't exist in Figma, it won't exist in code either; it'll get invented on the fly by whoever builds it, and it won't match the rest of the system.
Tokens Before Components
Once structure is settled, I export tokens — not manually, as CSS variables that mirror the Figma variables one-to-one. Color, spacing, radius, type scale. If a value in the design doesn't map to a token, that's a signal to fix the design, not to hardcode a one-off value in Tailwind.
// tailwind.config.ts — tokens generated from Figma variables,
// never the other way around
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
muted: "var(--muted)",
border: "var(--border)",
},This step matters more than the plugin you use to do it. I've seen teams spend weeks
evaluating Figma-to-code plugins while shipping components full of #F4F4F5 and
padding: 13px — the tool was never the bottleneck. The missing token was.
Build the Skeleton Before the Style
When I move into React, the first commit is never styled. It's structure: the right
HTML elements, the right component boundaries, real props with real TypeScript types,
placeholder content that's deliberately ugly. A <button> where the design has a
button. A <ul> where the design has a list, even though it doesn't look like one.
type CardProps = {
title: string;
description?: string;
image?: { src: string; alt: string };
actions: CardAction[];
};
export function Card({ title, description, image, actions }: CardProps) {
return (
<article className="card">
{image && <img src={image.src} alt={image.alt} />}
<h3>{title}</h3>
{description && <p>{description}</p>}
{actions.length > 0 && (
<ul>
{actions.map((action) => (
<li key={action.id}>{action.label}</li>
))}
</ul>
)}
</article>
);
}Styling an unstructured component is how you end up with a <div> wearing a button's
clothes — clickable, styled, and invisible to a screen reader or a keyboard. Structure
first means accessibility isn't a separate pass at the end; it's just what the component
already is by the time styling starts.
Style With Variants, Not With Copies
This is where most Figma-to-React handoffs quietly break down. The design file has a
"Primary Button" and a "Secondary Button" as two separate components. The code ends up
with Button and SecondaryButton as two separate files, because that's literally what
was handed off.
I do the opposite in both directions: one component, variant-driven, on both sides of the fence.
const buttonVariants = cva("inline-flex items-center rounded-md font-medium", {
variants: {
intent: {
primary: "bg-foreground text-background",
secondary: "bg-muted text-foreground",
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-base",
},
},
defaultVariants: { intent: "primary", size: "md" },
});If Figma has a variant, React has a variant prop. If Figma doesn't have a variant for something, I don't invent one in code — that's usually a sign the design system is missing a decision, and I go back and make it, rather than let the fork start.
The Part Nobody Puts in the Tutorial: Content That Breaks Things
The Figma file has "Siamak Mokhtari" as a name. Production has a user whose name is one character, and another user whose name is 340 characters because they pasted their bio into a field with no validation. Every card, every table row, every avatar stack needs to survive both.
This is where I spend the most real engineering time, and it's the part that never shows up in a design-to-code demo because demos use clean data on purpose. Before I call a component done, I run it against:
- An empty string, and
undefined, for every optional field - A string long enough to force wrapping and truncation
- Zero items, one item, and enough items to force scrolling
- A slow network — does the loading state actually match the Figma loading variant, or did someone just throw a spinner in because building the real skeleton felt like overkill
If the design system has an empty state and a loading state as first-class Figma components — and it should — this step is fast. If it doesn't, this is where the schedule slips, and it's almost never the engineer's fault when it does.
Where This Actually Saves Time
None of this is about moving faster on the first component. It's about not rebuilding the tenth one. A design system built this way — tokens first, structure before style, variants instead of copies, edge cases tested before "done" — compounds. The eleventh card component is a prop change, not a new file.
That compounding is the entire argument for doing the Figma side of this properly. The React code was never really the hard part. The hard part is making the design specific enough that the code has nothing left to guess.