Copy an icon from Figma, press Ctrl+V in the Webflow Designer, and nothing happens. The Designer only accepts its own elements. This is how its clipboard works, and how I convert SVG and HTML into it.

I run devpack, a site of free browser tools, and a lot of them are for Webflow. Webflow’s Designer is great when everything starts inside Webflow. It gets awkward when it doesn’t: an icon from Figma, a section someone wrote in plain HTML and CSS, a block of code ChatGPT gave you.

You copy it, click into the Designer, press Ctrl+V, and nothing happens. The Designer only pastes its own elements.

The usual workaround is a Code Embed. Paste the SVG or HTML into an Embed element and it shows up on the page. But an Embed is a black box. You can’t select the path inside it, you can’t give it a class, and the styles don’t show up in the Style panel. With twenty icons, you have twenty little boxes of code to keep in sync.

So I built two tools, SVG to Webflow and HTML & CSS to Webflow, and then a browser extension that does the same thing right inside the Designer when you paste. This is how they work.

What Webflow puts on the clipboard

The first step was to find out what Webflow pastes. When you copy an element in the Designer, it doesn’t put HTML on the clipboard. It puts JSON there, under the application/json type (and the same text as text/plain). You can see it by pasting into any text editor.

Trimmed down, a copied div with a class looks like this:

{ "type": "@webflow/XscpData", "payload": { "nodes": [ { "_id": "4f1c…", "type": "Block", "tag": "div", "classes": ["9a2e…"], "children": [], "data": { … } } ], "styles": [ { "_id": "9a2e…", "type": "class", "name": "card", "comb": "", "styleLess": "padding: 24px; border-radius: 12px;", "variants": { "small": { "styleLess": "padding: 16px;" } }, "children": [] } ], "assets": [], "ix1": [], "ix2": { … } }, "meta": { … } } 

A few things stood out:

  • Elements are a flat list of nodes. Each one has an id, and parents list their children by id. Text is its own node too, with text: true.
  • Classes live in a separate styles list, and the CSS is a plain string called styleLess.
  • Breakpoints and states are variants, keyed by names like medium, small, tiny and main_hover. If I could build this JSON myself and put it on the clipboard, the Designer would paste it like anything you copied in Webflow.

Putting JSON on the clipboard from a web page

This sounded like the easy part. It wasn’t.

The modern Clipboard API, navigator.clipboard.write(), only lets a page write a short list of types: plain text, HTML and PNG images. Chrome also allows custom types, but only with a "web " prefix, and Webflow doesn’t read those. There’s no way to write a plain application/json entry with it.

The old way still works, though. If you handle the copy event yourself, you can set any type you like on the event’s clipboardData. Then you trigger a copy from a button click:

function copyAs(map) { let ok = false; const onCopy = e => { for (const [type, value] of Object.entries(map)) e.clipboardData.setData(type, value); e.preventDefault(); ok = true; }; document.addEventListener('copy', onCopy); try { document.execCommand('copy'); } finally { document.removeEventListener('copy', onCopy); } return ok; } copyAs({ 'application/json': json, 'text/plain': json }); 

execCommand is deprecated, but every browser still supports it for this, and for now it’s the only way to do it.

Turning an SVG into elements

Webflow has a Custom Element: an element where you choose the tag yourself. That’s what makes SVG possible. An <svg> becomes a Custom Element with the tag svg, every <path> becomes a Custom Element with the tag path, and all the attributes (viewBox, d, fill…) go into the element’s attribute list. In the Navigator you get an svg element with its paths inside, and you can select and style each one.

The SVG to Webflow tool: SVG code exported from Figma on the left, and a preview of the icon with the cleaned SVG on the right

Before converting, the SVG needs cleaning. What you copy out of Figma or an icon site carries a lot of extras:

  • Editor attributes like data-name, sodipodi:*, inkscape:* and figma:*, plus comments, <metadata> and <title>. They go.
  • Anything that can run code: <script>, <foreignObject>, on* attributes and javascript: links. They go too.
  • xlink:href. This one took me a while. The Designer recreates the elements without the xlink namespace, so a <use xlink:href="#icon"> silently shows nothing. Rewriting it as a plain href fixes it. There are two options I added because I kept wanting them myself. currentColor replaces every fill and stroke color with currentColor, so the icon follows the text color you set in Webflow. And a class name and size option puts the width and height on a Webflow class instead of the SVG, so all your icons share one class you can change in one place.

The format isn’t documented, and it changes

None of this JSON is documented, and Webflow can change the exact shape of a Custom Element whenever it likes. A converter that hard-codes it breaks quietly one day.

So the tools can learn it. If you copy any Custom Element in the Designer and paste it into the tool (or, with the extension, paste it back into the Designer), it reads the element and uses it as a template. The tricky part is finding where the attributes live, because that key could move. I search for it instead of hard-coding it: the first array whose items all look like { name, value }.

function findAttrPath(obj, path = []) { for (const [k, v] of Object.entries(obj)) { if (Array.isArray(v) && v.length && v.every(x => x && 'name' in x && 'value' in x)) return [...path, k]; if (v && typeof v === 'object' && !Array.isArray(v)) { const found = findAttrPath(v, [...path, k]); if (found) return found; } } return null; } 

Every new element is a copy of that template with a fresh id, its own tag and its own attributes. If Webflow changes the format, one copy and paste updates it.

HTML and CSS: classes, breakpoints and hover states

SVG was the warm-up. HTML with CSS is where it gets interesting, because the goal isn’t just elements. It’s elements with real Webflow classes, so you can keep editing them in the Style panel.

The elements part is a walk over the parsed HTML. In “native” mode, tags map to Webflow’s own elements: div, section, header and the other layout tags become Div Blocks that keep their tag, h1–h6 become Headings, p a Paragraph, a a Link, img an Image, and lists become List and List Item. Anything else, including inline SVG, becomes a Custom Element with its exact tag. There’s also a “custom” mode that keeps every tag exactly as written.

The CSS is the hard part, because Webflow and CSS think about styles differently:

  • CSS is a cascade of rules. Webflow has one style per class. A class can be set by several rules in your CSS, and the order and specificity decide which values win.
  • Your media queries can be anything. Webflow has fixed breakpoints, and they’re desktop-first. The base style is desktop, and each smaller breakpoint (991px, 767px and 479px wide) only stores what changes.
  • Combo classes. .button.is-secondary in CSS is a combo class in Webflow, which is a separate style that belongs to its parent class. So instead of translating rules one by one, I work out what each class actually looks like at each breakpoint. For every class, I pick one test width per breakpoint (1200, 900, 600 and 400 pixels), collect all the rules that apply at that width, sort them by specificity and order, and get the final values. That gives me the full desktop style. For each smaller breakpoint, I store only the values that differ from the one above it:
const BANDS = [{ key: 'main', w: 1200 }, { key: 'medium', w: 900 }, { key: 'small', w: 600 }, { key: 'tiny', w: 400 }]; const effs = BANDS.map(b => effectiveStyle(cls, b.w)); // final values at each width const variants = {}; BANDS.forEach((b, i) => { if (!i) return; const changed = diff(effs[i], effs[i - 1]); // only what changes if (Object.keys(changed).length) variants[b.key] = { styleLess: toLess(changed) }; }); 

This handles max-width and min-width queries, rems and ems, and queries written in any order, because I never try to map a query to a breakpoint directly. I just test each width. :hover, :focus and :active work the same way and become Webflow states like main_hover. Shorthands get expanded too, since Webflow stores longhands: padding: 8px 16px becomes four separate values. var(--…) values are resolved from :root.

Some CSS just can’t become a Webflow class: element selectors like h1 { … }, descendant selectors, @keyframes and media queries for screens wider than Webflow’s base breakpoint. I don’t want to throw that away silently, so it goes into a Code Embed called “Custom CSS” that’s pasted along with the elements. The tool also shows a report: how many elements, classes and combo classes it created, and a warning for every rule it couldn’t map, every image with a relative URL (Webflow needs absolute ones) and an Embed that’s over Webflow’s 50,000-character limit.

The HTML and CSS to Webflow tool after converting a hero section: a preview of the section, then 12 elements, 11 classes, 1 combo class, 4 breakpoint and hover styles, and a report that says everything mapped to native Webflow classes

The extension: catching the paste

The web tools have one annoying step: you paste into the tool, click Copy, switch tabs, then paste into Webflow. I wanted Ctrl+V in the Designer to just work. That needs a browser extension, because only an extension can run code on the Designer’s page.

The extension’s content script listens for paste in the capture phase, before the Designer sees it, and decides what’s on the clipboard:

  • Only an SVG? Convert it to elements.
  • Text that looks like HTML code? Convert it with the CSS from its <style> tags.
  • Rich HTML from a web page, like part of an article? Convert it to clean Headings, Paragraphs, Lists, Links and Images, without the page’s classes and styles.
  • Anything else is left alone: real Webflow data, plain text, pastes into text fields and code editors, and rich copies from VS Code or GitHub, which are code, not content. Then it stops the original paste and fires a new one, carrying the converted JSON:
window.addEventListener('paste', e => { if (!e.isTrusted) return; // our own paste passes straight through const what = classify(e.clipboardData); if (!what) return; e.preventDefault(); e.stopImmediatePropagation(); const json = convert(what); const data = new DataTransfer(); data.setData('application/json', json); data.setData('text/plain', json); e.target.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true })); }, true); 

The isTrusted check matters. The event I dispatch is untrusted, so the listener ignores it and doesn’t loop forever, and the Designer handles it like a normal paste.

Firefox needed a different route. There, a page can’t read the data of a paste event that an extension created, so the hand-off arrives empty. In Firefox the extension puts the converted JSON on the real clipboard instead, using the same copy-event trick as the website, and shows a message: press Ctrl+V again. The first paste converts, the second one pastes.

It also keeps a small component library. When you copy elements in the Designer, it offers to save them, and Alt+Shift+L opens your saved components in any project. I won’t go into that here, it’s a post of its own.

The extension asks for one permission, storage, for your settings and library. It only runs on Webflow Designer pages and devpack.app, and it makes no network requests. Everything is converted on your machine.

One more catch: two browsers

Custom clipboard types don’t travel between browsers. If you copy in Chrome and Webflow is open in Firefox, or the other way around, the JSON arrives as plain text and the Designer ignores it.

For that I made a small page, the Webflow Clipboard Bridge. Open it in the browser where Webflow is, paste there, and it copies the data again in that browser’s own format.

What doesn’t work (yet)

  • Element selectors, descendant selectors and animations end up in the Custom CSS embed, not in Webflow classes. Inline style attributes are dropped with a warning, so move them into a class first.
  • Images need to be uploaded to Webflow afterwards. The converter keeps absolute image URLs and warns about relative ones.
  • Webflow can change its clipboard format. The template learning covers Custom Elements, but a big change would still need an update from me.

If you’re building something similar

  • Look at the clipboard first. Paste what an app copies into a text editor. Many apps use plain JSON you can generate yourself.
  • To write a custom type from a page, handle the copy event. navigator.clipboard.write() won’t do it.
  • Don’t translate CSS rule by rule. Work out the final values at each breakpoint and store only the differences.
  • Don’t hard-code an undocumented format. Learn it from a real sample, and search for the parts you need.
  • In an extension, re-dispatch the paste and check isTrusted. And test Firefox early, because it handles synthetic clipboard events differently. The tools are free: SVG to Webflow and HTML & CSS to Webflow. The extension is free too, for Chrome, Edge and other Chromium browsers and for Firefox. If you have an SVG or a bit of HTML that doesn’t convert well, I’d like to see it.

Originally published on the devpack blog.