A Chrome extension, a Flutter package and a Figma plugin that turn a live UI into real Figma frames, auto layout, text, images and vectors. Not a screenshot — every layer is editable.
No account. No server. No sign-up. No export limits.
html to figma, website to figma, web page to figma — whatever you call it, this does it locally on your machine:
- You open any page in Chrome and click the extension.
- It reads the live DOM and computed styles — every box, colour, font, shadow and image.
- It puts a compressed snapshot on your clipboard.
- You paste in Figma and the companion plugin rebuilds it as real, editable layers.
Chrome extension clipboard Figma plugin
──────────────── ───────── ────────────
pick viewports + themes ─────► C2D1:<gzip+base64> ─────► Ctrl+V, run plugin
walk DOM + computed CSS frames / text / images
fetch images as bytes auto layout, hyperlinks
If you have used html.to.design, this is the same idea, built in the open and running entirely on your own machine.
Most website-to-Figma tools route your page through someone else's servers and meter how many imports you get. This one does not have servers to route through:
| code.to.design | |
|---|---|
| Cost | Free, forever — it is MIT-licensed source code |
| Import limit | None. There is no quota to enforce because there is no backend |
| Account | None. Nothing to sign up for |
| Where your page goes | Nowhere. The capture never leaves your computer |
| Pages behind a login | Work fine — it reads the page your browser is already showing |
| Internal / localhost sites | Work fine, offline included |
| Source code | All of it, right here |
The whole pipeline is your browser → your clipboard → your Figma. The Figma plugin declares
"networkAccess": { "allowedDomains": ["none"] }, so it is not able to phone home even if
it wanted to.
- Install
- How to use it
- Flutter apps
- What comes across
- What does not
- FAQ
- How it works
- Development
- Contributing
- License
You need Node.js 20+, Chrome 116+, and the Figma desktop app.
(The test suite executes TypeScript directly, so running npm test needs Node 22.6+.)
git clone https://github.com/naqiiabbas/code.to.design.git
cd code.to.design
npm install
npm run buildThat produces two folders:
| Folder | What it is |
|---|---|
packages/extension/dist |
the unpacked Chrome extension |
packages/figma-plugin/dist |
the Figma plugin |
npm run package also zips both into release/, which is what you send a teammate.
- Open
chrome://extensions - Turn on Developer mode (top right)
- Load unpacked → select
packages/extension/dist - Pin it: puzzle-piece icon in the toolbar → pin code.to.design
Figma does not accept a zip directly, but a local install takes four clicks and needs no publishing, no review and no approval:
- Open the Figma desktop app (development plugins do not run at figma.com in a browser)
- Menu → Plugins → Development → Import plugin from manifest…
- Pick
manifest.jsoninsidepackages/figma-plugin/dist
It now lives under Plugins → Development → code.to.design, permanently. To share it,
send someone release/code-to-design-figma-plugin.zip and have them unzip it and import the
manifest the same way.
Once both halves are installed, converting a website to a Figma design takes about ten
seconds. Open any page in Chrome and click the code.to.design icon. Tick the
viewports you want — Browser is whatever your window is right now, and the others
(1920, 1440, 1024, 768, 390) are emulated properly through Chrome DevTools so the page's own
media queries respond and you get the genuine mobile layout rather than a squashed desktop
one. Tick the themes you want too (Browser, Light, Dark), and every combination you
tick becomes its own Figma frame in a single import. Then press Capture Current Page
(Alt+Shift+E) for the whole scrollable page, or Capture Selection (Alt+Shift+D) to
pick one component: the popup closes, whatever you hover is outlined in blue with its tag
and pixel size, ↑/↓ step to the parent or first child, and a click confirms. The capture
lands on your clipboard. Switch to Figma, press Ctrl+V on the canvas, then run the
plugin — Ctrl+Alt+P re-runs the last plugin, so after the first time it is two keystrokes.
The plugin picks the pasted payload up, rebuilds it as layers, cleans up after itself and
closes without ever opening a window. Repeat as often as you like; nothing is counted.
Keyboard shortcuts
| Shortcut | What it does |
|---|---|
Alt + Shift + E |
Capture the current page |
Alt + Shift + D |
Capture a selection (element picker) |
Ctrl + V / Cmd + V |
Paste the capture onto the Figma canvas |
Ctrl + Alt + P / Cmd + Opt + P |
Re-run the last Figma plugin (imports it) |
↑ / ↓ |
While picking: step to parent / first child |
Esc |
Cancel the picker |
If the clipboard struggles on a very large page, use Save .c2d file in the popup and drag that file onto the plugin's panel instead. Running the plugin before pasting also works — it opens a panel with a paste area, which is the fallback when there is nothing on the canvas to pick up.
Flutter Web cannot be captured through Chrome: CanvasKit paints the whole app into one
<canvas> inside a shadow root, so there is no DOM to read. Measured on Flutter 3.44, a
running app exposes 29 DOM elements, none of them content, and zero text.
So the Flutter half reads Flutter’s own render tree instead, which is strictly better:
exact geometry from localToGlobal plus size, and widgets that already say what they are.
It emits the same clipboard payload, so the Figma plugin imports it unchanged.
dependencies:
code_to_design: { path: packages/flutter_capture }
// main.dart — debug builds only, compiled out of release
runApp(const CaptureOverlay(child: MyApp()));Icons and leaf CustomPaints are rasterised to transparent PNGs, since a glyph from an icon
font would otherwise arrive in Figma as a random letter. Only the screen you are looking at is
captured: Flutter keeps a lot alive that nobody can see — routes you pushed over, every tab of a
bottom navigation bar, rows past the end of a list — and each is filtered out. A page behind a
dialog is kept, because that one really is on screen.
The screen's own background colour comes across, so a dark-themed app no longer lands on white.
Row and Column keep their spacing:. Layers are ordered by the order they are actually
painted in rather than the order they are listed — which is what used to drop a text field's
fill on top of its own placeholder. And text is imported hugging its content, so a font Figma
had to substitute cannot make a line wrap.
Press Capture to Figma, then Ctrl+V on the canvas and run the plugin. Works on Web,
Windows, macOS, Linux, Android and iOS. Row/Column become auto layout, BoxDecoration
becomes fills and shadows, TextStyle becomes real text styling, and the ~90% of a Flutter
tree that paints nothing (Align, Padding, Semantics…) is pruned away — a Material
screen goes from 130 layers to 15 without moving a pixel.
Full details: packages/flutter_capture.
| Web | Figma |
|---|---|
| element boxes | frames with exact size and position |
display: flex containers |
auto layout (toggleable in the plugin) |
background-color, linear/radial/conic-gradient, background-image |
fills, in CSS paint order |
border (per-side widths, dashed, dotted) |
strokes, inside-aligned |
border-radius |
per-corner radii |
box-shadow, text-shadow, filter: blur, backdrop-filter: blur |
drop/inner shadows, layer blur, background blur |
| text with mixed inline styling | one text layer with per-range font, size, weight, colour, spacing, decoration, case |
background-clip: text gradient headings |
gradient text fills |
<img>, <canvas>, <video> frames, CSS url() |
image fills, pixel-exact — nothing is lossily re-compressed |
inline <svg>, sprite icons (<use href="#id">), .svg sources |
real vector layers |
<a href> |
Figma hyperlinks |
opacity, mix-blend-mode, transform: rotate() |
opacity, blend mode, rotation |
<input>, <textarea>, <select> |
box plus its value or placeholder as text |
overflow: hidden/auto |
clipping frames |
| internally scrolling boxes (dropdowns, popup lists, horizontal strips) | frames grown to their full content, with everything after them reflowed |
A few things worth knowing about how it gets there:
- Modern colour spaces are handled properly. Colours are resolved by painting a pixel
and reading it back, not by parsing what
getComputedStylereturns — the only approach that survivesoklch(Tailwind v4's entire palette),lab,lch,oklab,color(srgb …),display-p3andcolor-mix(). - Images are never re-compressed. PNG, JPEG and GIF are handed to Figma as the exact
bytes the site served. WebP and AVIF, which Figma cannot read, are decoded and re-encoded
as lossless PNG. An
<img>with asrcsetis fetched at its sharpest candidate. - Scrolling content is captured in full. Every scroller on the page is walked end to end first — not just the window, which on an app shell with a fixed-height body does not scroll at all — so lazy images and scroll-triggered content have rendered. Boxes with their own scrollbar are then temporarily expanded, so a dropdown showing 3 of 10 rows arrives with all 10 and a frame tall enough to hold them.
- Fonts are matched by family and numeric weight against what Figma actually has installed, falling back through the CSS font stack and then to Inter. Every substitution is reported after import.
Real limitations, listed honestly. Each one is reported in the plugin's post-import notes when it comes up:
- In-flow
::before/::after. Only absolutely positioned pseudo elements are captured — no DOM API reports the geometry of an in-flow one, so icon fonts driven bycontent:in normal flow will be missing. <iframe>content. Cross-origin frames cannot be read; you get a placeholder.- List markers (
<li>bullets and numbers) are not drawn. - CSS grid is captured with absolute positions rather than converted to auto layout.
visibility: hiddensubtrees are skipped whole.- Repeating gradients fall back to a single pass of their stops.
- Animations are paused at whatever frame the page was on.
- Endless feeds are captured as far as 8 seconds of scrolling reaches. Scroll further yourself and capture again for the rest.
- Tainted
<canvas>(drawn from cross-origin content) cannot be read. - Shadow DOM. Content inside a shadow root is not traversed yet, so pages built from web components come through as empty boxes.
- Canvas-rendered apps (Unity,
<canvas>-based editors) have no DOM to read. Flutter is handled separately — see Flutter apps.
Is it really free? What's the catch? It is MIT-licensed source code that runs on your machine. There is no backend to pay for and therefore no quota to sell you. Capture as many pages as you like.
Does my page data go anywhere? No. The capture goes browser → clipboard → Figma, entirely locally. The Figma plugin declares no network access at all. The extension only makes requests to fetch the images the page itself already loaded.
Can I capture pages behind a login, or on localhost?
Yes. It reads the page your browser is already showing, with your session, so authenticated
dashboards and localhost both work.
Why does it need the debugger permission?
Only to emulate viewports and themes, via Emulation.setDeviceMetricsOverride and
setEmulatedMedia — the only way to make the page's own media queries respond so you get a
genuine mobile layout. Chrome does not allow debugger to be an optional permission (it
silently drops it), so it must be declared up front. It is only used when you tick a
non-browser viewport or an explicit theme; capturing with Browser + Browser theme never
attaches at all. When it does, Chrome shows its own banner on the tab and the extension
detaches as soon as the capture ends.
Why does my text look different in Figma? Figma can only use fonts installed on your computer. If the site uses a webfont you do not have, Figma substitutes Inter or Roboto. The plugin tells you exactly which fonts it swapped after each import — install those locally and re-import for an exact match.
It looks blurry when I zoom in Figma. Is the capture low quality? Almost certainly not. Figma draws a low-resolution proxy while it catches up on a heavy frame, and the more layers a frame holds, the longer that takes. Wait a few seconds after zooming, or export the frame as PNG at 2x/4x — the export is the file's real quality.
Can it capture a Flutter Web app (or Unity, or a canvas-based editor)?
Not as editable layers, no. Modern Flutter Web renders everything with CanvasKit into a
single <canvas> inside a shadow root — measured on Flutter 3.44, a running app exposes 29
DOM elements, none of them content, and zero text. There is simply nothing to read: no
boxes, no fonts, no colours, just painted pixels. The same is true of any canvas-rendered
app. A capture of one currently produces a handful of empty frames. Design-to-code tools can
go the other way; going from a canvas back to structured layers is not something the DOM can
answer.
Can I use it with Figma in a browser? The plugin needs the Figma desktop app, because that is where development plugins run. The Chrome extension side works anywhere.
Does it work on macOS / Windows / Linux? Yes, anywhere Chrome and the Figma desktop app run.
Why isn't it on the Chrome Web Store / Figma Community? It is distributed as source you build and load yourself, which is what keeps it free of accounts and limits. Anyone is welcome to publish a fork.
packages/shared/ snapshot schema + clipboard codec, shared by both halves
packages/extension/ MV3 Chrome extension
src/popup/ React popup (viewports, themes, the two capture buttons)
src/background/ orchestration, DevTools emulation, image fetching, clipboard
src/capture/ the DOM walker injected into the page
packages/flutter_capture/ Dart package: walks Flutter’s render tree, same payload
packages/figma-plugin/ Figma plugin
src/ui/ React iframe: paste, decode, options
src/code/ sandbox: font resolution and node construction
The capture bundle runs inside the page, but every image is fetched by the service worker instead. That is deliberate: the worker holds host permissions for all origins, so cross-origin images come back as real bytes rather than tainting a canvas.
The suite runs the real built bundles, not mocks talking to each other. It launches your
installed Chrome, injects the actual capture.js into fixture pages, pushes the result
through the real clipboard codec, and runs the actual plugin bundle against a deliberately
strict Figma API stand-in that throws wherever Figma throws — characters set before the font
is loaded, a non-PNG/JPEG/GIF handed to createImage, alignment set on a frame with no
layoutMode, a paint channel outside 0..1. With auto layout off, the resulting Figma tree is
compared to the capture node for node. The element picker is verified by screenshotting
the page and sampling real pixels, because a DOM assertion cannot tell you whether a human
can see the highlight.
What it does not prove: the stand-in enforces Figma's rules, it does not render. Nothing in the suite confirms a gradient looks right on the canvas.
npm run build # build both halves, plus icons
npm run build:ext # extension only
npm run build:plugin # plugin only
npm run typecheck # all packages
npm test # unit + end-to-end tests (drives real Chrome)
npm run test:e2e # end-to-end only
npm run test:flutter # the Dart package (needs Flutter installed)
npm run package # zip both dist folders into release/After rebuilding: hit Reload on the extension card in chrome://extensions, and in Figma
just run the plugin again — development plugins re-read from disk on every run.
npm auditreports a moderate advisory against esbuild's dev server, reached through Vite. This project only ever runsvite build, nevervite dev, so that server is never started.
Issues and pull requests are welcome. Useful things to know:
- Run
npm testbefore opening a PR — it drives real Chrome, so it catches real regressions. - New behaviour needs a test that fails without the change. Several bugs in this codebase were found because a test was checked against the broken build first.
- Fixture pages live in
packages/extension/test/*.html. Adding a page that reproduces a site you had trouble with is a genuinely useful contribution on its own.
Good first issues: CSS grid → auto layout, <li> list markers, in-flow pseudo elements.
MIT — do whatever you like with it, including commercially.
Keywords: flutter to figma · html to figma · website to figma · web page to figma · convert website to figma · import website into figma · figma plugin · chrome extension · design to code · web to design · open source html.to.design alternative · free website to figma converter
If this saved you some time, a ⭐ helps other people find it.