← Blog

2025-05-28

From Figma to JSON to HTML

Every “Figma to code” tool I’ve tried does the same thing: it goes straight from a design file to a finished React component in one opaque hop. You get something that half-works, styled with a pile of magic numbers, and the moment the design changes you’re back to hand-editing pixel values off the canvas. The tool optimized for the wrong thing: it tried to be smart about code generation instead of being honest about the data.

So I split the problem in two. First, get the design out of Figma as clean, structured data, nothing lossy and nothing guessed. Then, separately, decide what to do with that data. Maybe it’s HTML. Maybe it’s Pug. Maybe it’s context for an LLM that’s writing SwiftUI. The point isn’t the output format, it’s having a stable intermediate you can actually inspect and trust.

That’s figma-to-json (the extractor) and figma2html (one consumer of what it produces).

Step one: export the tree, not a guess

figma-to-json is a Figma plugin. This part isn’t clever; it leans directly on how Figma models a document. From the Figma plugin docs:

Figma, FigJam, Figma Slides, and Figma Buzz files are structured as node trees. Your plugin can traverse nodes in the tree in order to access different parts of the document.

So the plugin walks that node tree recursively and pulls out everything Figma already knows about each element: id, name, type, dimensions, fills, strokes, effects, opacity, blendMode, layout mode and alignment, per-corner radii, typography for text nodes, and the full children array preserving hierarchy. No inference, no “this looks like a button so I’ll assume it’s clickable.” Just what’s in the file. As of v1.1.0 it also exports images and SVGs as PNG in the JSON.

The one genuinely clever bit is variable handling. If a fill is bound to a Figma variable, the JSON doesn’t resolve it to a hex value and throw the binding away. It keeps the resolved value and the variable name and which collection it came from:

{
  "cssProps": {
    "background-color": "#ff0000",
    "background-color-variable": "--theme-primary-color",
    "background-color-collection": "Theme Colors",
    "background-color-variable-id": "VariableID:123"
  }
}

That collection field exists because Figma lets two different variable collections define a variable with the same name; resolve naively and you’ll silently collide. Small detail, but it only shows up once you’ve run this against a real design system with multiple themes, not a toy file.

The plugin also has an error path most export tools skip: if a node crashes the exporter (some nested instance with a weird override, usually), it surfaces the error in the UI and gives you a “Copy Node Data” button so you can grab the exact structure that broke it. Design files are messier than the Figma API docs suggest, and pretending otherwise just means silent failures later.

Step two: turn the JSON into something else

figma2html takes that JSON and walks it again, this time emitting markup. FRAME and INSTANCE nodes become <div>s, TEXT becomes <p>, RECTANGLE becomes <div>, and every resolved CSS prop from the JSON gets applied as an inline style. The Figma layer name goes on as data-figma-name, the type as data-figma-type, and instances get data-figma-instance="true" so you can tell which parts of the tree came from a component instance versus a one-off shape.

const figmaJson = JSON.parse(readFileSync('export.json', 'utf8'));
const converter = new FigmaToHtmlConverter();
const html = converter.convert(figmaJson);   // or converter.convertToPug(figmaJson)

There’s also a CLI (npx figma2html input.json --pug) for when you just want to pipe a file through without writing any code.

Where this actually breaks

Inline styles with no cascade means no media queries, no hover states, no pseudo-elements. You get the frame at the size and state it was in when you exported it. Auto-layout gets translated, but Figma’s layout model and CSS flexbox/grid aren’t identical, so nested auto-layout frames sometimes need a manual pass. And this exports appearance, not behavior: you still wire up interactivity by hand, because Figma doesn’t know what your button does when clicked.

None of that is a dealbreaker, because none of it is the job. This pipeline has a narrower goal: getting you from “here’s a Figma frame” to “here’s a faithful static representation I can keep transforming” without a human reading pixel values off the canvas. The HTML is a genuinely useful scaffold or a visual-diff baseline. And because the intermediate is just JSON, it composes with anything: the same export that feeds figma2html can be handed to an LLM as grounded context for a SwiftUI or Compose translation, which is closer to how I actually use it day to day.

The lesson that generalizes past Figma: when a “translate A to B” problem feels lossy and unreliable, check whether you’re really doing two things in one step. Separate the extraction from the transformation, make the intermediate something you can inspect and trust, and the “translation” part gets a lot less magical, and a lot more correct.

So draw the seam on purpose: separate extraction from transformation, and keep an inspectable intermediate between them instead of letting the codegen swallow it whole.

References