Back to devlogs
Typescript · HTML · CSS

Updating Design to HTML: semantic tags and full-width sections

/ / 4 min read

The second pass on my Figma to code plugin: real HTML tags chosen without AI, mixed text as spans, and sections that reach the screen edges.

Design to HTML exporting a Figma frame to semantic HTML with full-width sections.
On this page

The first version of Design to HTML wrapped everything in a div. That is honest, and it is also a wall of nameless boxes. Then I exported a real page, my own portfolio, and two things stood out: the markup said nothing about what each part was, and every section stopped at the design width with the page color leaking down the sides. This update fixes both, plus a run of smaller bugs that only a real export surfaces.

Watch it work

A short walkthrough of the plugin: select a frame, export, and read the result. If the player does not load, watch it on YouTube.

Guessing the right tag, without a model

The plugin has no AI. It reads a frame and a name, and that is all it gets. So the semantic tags are a ladder of conservative guesses, and the neutral div or p always wins a tie.

Headings come from size, not names. I count every text size on the page, treat the most common one as body, then rank the sizes clearly larger than body and hand them out as h1 to h4. A heading also has to be short and single line, so a long paragraph set in a big font stays a p instead of becoming an h1.

Landmarks come from layer names. A frame named nav, footer, aside, or article maps to that element at any depth. Header, section, and main are common words in group names, so they only count for the page's top-level bands, not for a nested group called "name section".

Buttons are the careful case. A frame becomes a button only when its name says button, btn, or cta, and only when it does not already wrap other buttons. That last check stops a "button row" from turning into a button that contains buttons. I tried detecting buttons by shape, a rounded box with centered text, and dropped it, because a chip, a tag, and a badge all look the same and none of them are buttons.

One text node, many styles

A Figma text node can hold several styles at once: a bold label, a blank line, then a lighter paragraph. My first version read one font and one size for the whole node, so a heading and its body came out identical. Now I read the styled segments and emit a span per run, each with its own weight, size, and color. The block reads the way it was drawn.

What a real export breaks

Turning a div into a real element hands you the browser's defaults, and they are not subtle.

A button arrives with a gray fill, a beveled border, and its own font. My teal call to action gained a stray border, and a transparent, outline-only button turned into a pale box with text you could not read. The fix is a small reset that strips the fill, border, font, and appearance so the button is painted only by its own styles.

Icons were exporting as broken pieces. An icon built from several vector strokes, or a glyph sitting on a shape, was walking through the normal container path and each part landed in its own box. Now a container whose whole subtree is vector art, no text and no image, exports as a single SVG, the way Figma draws it.

Rounded corners were leaking. Figma clips a frame's content when you ask it to, and I only carried that clip onto auto-layout frames. A static card with rounded corners never clipped, so a full-bleed child squared off the bottom corners. Moving the clip to every frame fixed it.

Sections that reach the screen edges

This was the stubborn one. A section that is dark navy in the design should run edge to edge in the browser, not stop at 1440 pixels with the page color on either side.

The background was there, the section just could not escape its own frame. The exported root came out as a fixed 1440 pixel box with overflow: hidden, and every section carried the same clip. So any attempt to stretch a background hit that wall and got trimmed back to the column.

The fix is two moves. The page root no longer clips, so nothing is trapped in the box. A section that spans the page then drops its own clip and paints its background across the full viewport with a ::before, while its content stays in the centered column. Colors carry to both edges, and narrower or left-aligned blocks are left alone.

What I checked

The harness from the first version grew with the work. It stubs the figma global, runs a portfolio-shaped tree through the compiled plugin, and now holds 51 assertions: headings by size, landmarks by name, buttons that never nest, mixed text split into spans, icons flattened to one SVG, static frames that clip, and sections that bleed to the edge with the root no longer trapping them. Plain Node, no framework. Each fix landed with the check that fails if it regresses.

Where it is

These changes sit on a branch while I test exports against real files. The plugin itself is on the Figma Community and the code is on GitHub, free to use.

FAQ

Frequently asked

Semantic tags instead of div everywhere, mixed text split into spans, and full-width sections that reach the screen edges, plus fixes for button defaults, multi-part icons, and rounded-corner clipping.

It ranks text sizes to assign h1 to h4, reads layer names for landmarks like nav and footer, and marks a frame as a button only when its name says so and it does not wrap other buttons. When no signal is strong, it keeps the neutral div or p.

The exported root was a fixed 1440 pixel box with overflow hidden, and every section inherited that clip, so any full-width background was trimmed to the design column. Now the root never clips and a full-width section paints its background across the whole viewport with a ::before while its content stays centered.

Following along? Let's talk.

Start a conversation →