Vellum
Figma plugin · solo product · Alexey Sukhariev · Design engineer
A Figma plugin that imports a live website into Figma as native, editable layers — measured from the browser’s own layout engine instead of traced by eye. Built alone in 5 weeks: the import engine, the brand, the pricing and the launch. Published on the Figma Community with payments live and real transactions processed end to end.
Import the web. Pixel for pixel.
Vellum is a Figma plugin. Give it a live URL, an HTML file, or pasted CSS, and it rebuilds the page inside Figma as native layers — real frames, text, vectors and image fills, placed where the browser draws them. Nothing is flattened into a bitmap, and nothing is approximated. I built it alone in about 5 weeks — 383 commits between 22 May and 25 June 2026 — and that scope included the import engine, a serverless rendering backend, licensing, payments, the brand, the landing page, a 24-post blog and the Figma Community listing.
Why this is hard
Designers need an existing website inside Figma constantly: to redesign it, audit it, reuse a component, hand it off. Every existing option fails in one of two ways. Screenshot tools return a flat bitmap — accurate and useless, because nothing in it can be edited. Converters guess: they approximate the page into rectangles, or ask a model to invent the structure, and the output drifts away from the page it claims to reproduce.
The browser already solved layout
The insight the whole product stands on: nothing needs to be inferred. The browser has already resolved every coordinate, every font weight, every line-height, every computed colour — it does that before it paints a single pixel. So Vellum does not re-derive the layout. It reads the measurements out of the render and rebuilds them as Figma nodes. That sounds simple. The next four sections are why it is not.
The first engine, and why I threw it away
The first version rendered the target page in a sandboxed iframe inside the plugin’s own UI and walked that DOM. It demoed beautifully and collapsed on real sites. A Figma plugin’s iframe loads from a data: URL, so its origin is null — every stylesheet and font request went out with Origin: null, CDNs answered partially, and theme CSS applied unevenly. A common sidebar pattern created a feedback loop the settled browser resolves and the plugin captured mid-flight, 1,200 px too tall. Auto Layout only fired on display: flex, which misses most real pages. Off-screen menus hidden at left: -10989px were serialised as real nodes 10,000 px away from the import.
After roughly 30 commits of point fixes, the diagnosis changed: the bug was not in the code, it was the location of the render. Version 2 moved rendering to a serverless function running headless Chrome — it loads the real page, waits for the network and animations to settle, freezes them, reads the computed layout of every node and returns a JSON tree. The plugin builds Figma nodes from that tree. One move removed an entire class of bugs instead of patching its symptoms.
The cascade
This was the defining problem of the project, and a good example of why fidelity work is brutal: a text node that re-wraps does not cause a small error, it causes total failure. A label captured as fitting in 162 px re-wrapped to two lines inside Figma; the child grew, so its parent grew, so every sibling below it shifted, and the whole page plowed.
The obvious fix — find a better font match — does not exist. Apple’s site uses SF Pro, and a Figma plugin cannot load a font that is not installed: the API explicitly does not fetch fonts from the internet, there is no font-registration API, the licence forbids shipping the font, and no metric-compatible clone of San Francisco exists. There was no way to win on the font. So the fix moved to the layout. The server measures every visual line the browser produced, bakes those line breaks in as hard newlines, and gives the node a fixed width and height — Figma is never allowed to re-wrap. Headings stopped breaking mid-word, and footer legalese stopped reflowing. Competitors solve the same problem by shrinking the type until it fits, which silently changes the design, or by telling the user to go and install the font.
A general fix, not per-site hacks
Overlay text was rendering behind the images it belonged on — on some sites, not others. The temptation in that situation is a per-site patch. I refused it: intermittent behaviour means the model is wrong, not that the site is exotic. The root cause was a mismatch between two painting models. Figma paints children in array order. CSS paints by stacking context — z-index, position, opacity, transform, filter, blend mode, isolation — and no open-source implementation reproduces the full CSS painting algorithm.
The shipped answer is a heuristic that holds generally: inside any parent, a child that contains no text anywhere in its subtree, has z-index auto or 0, and covers at least 85% of the parent on both axes is a background, and is ranked behind its text-bearing siblings. An explicit z-index is always honoured. Every hero tile on apple.com flipped from content-over-image to image-under-content, and the overlay copy and calls to action became visible.
What can’t be a layer
Some things are not layers, and pretending otherwise produces garbage: WebGL scenes, canvas, dense product mockups. The serializer detects those regions and bakes them as images while everything around them stays native. There is also an inverted mode that bakes the artwork underneath and keeps the text on top editable.
The rule for that came out of a specific failure. An Apple TV+ carousel was emitted as a single image 11,367 px wide — a 1,920 px source stretched about six times, with the section ballooning 9,000 px off-canvas. The screenshot had already been clamped to the viewport, but the node’s bounding box still carried the raw geometry from before the clamp. The fix was two lines. The generalisation was worth more: any raster region wider than the viewport times 1.3 is a horizontal-scroll carousel by definition, never a self-contained mockup — so its art gets baked underneath while its titles and buttons stay native and editable.
How correctness was proven
Fidelity claims are cheap, so the project grew its own proof methods. A fixture page carried 32 cards across 11 CSS effect families — gradients, shadows, blurs, blend modes, transforms, clip-paths, spinners — imported and scored card by card. Final score: 30 of 32. Imports were diffed against ground-truth screenshots as side-by-side sheets, and a regression suite of real sites was re-run after every batch of fixes.
One bug set the standing rule. A stray black dot kept appearing in an imported nav. It turned out to be a pseudo-element on an empty shopping-bag badge, hidden with transform: scale(0) — so display, visibility and opacity all read as perfectly normal while the rendered rectangle was 0 by 0. A search through the captured data reported the bug fixed when it was not; only re-rendering the actual Figma output caught it. Trust the render, never the tree data. The same discipline applies to what the product cannot do: the rendering environment has WebGL 1 but no WebGL 2, so 3D scenes never draw. Rather than ship black silhouettes, the serializer detects those regions and filters them out — the limit is named instead of faked.
The brand: name the failure
Vellum is the prepared surface you trace onto, and the metaphor runs through the whole system — tracing, measuring, drafting. The positioning line is “Import the web. Pixel for pixel.” The brand line is “Measure twice. Trace once.” The voice rules are written down and enforced: terse, technical, no emoji, no exclamation marks, no “AI”, no “magic”, numerals over words, past tense for completed work. One rule outgrew the style guide and became the product’s position: name the failure. If a font is missing or an image is CORS-locked, the product says so instead of faking it.
Type is Fraunces for display, Inter for interface, JetBrains Mono for code. The palette is “Ink on Vellum” — warm paper, near-black ink, one accent, which was deliberately repositioned mid-project from a drafting blue to Drafting Violet #7A2BC9 and carried through the plugin, the site, the banners and the checkout. Both themes are harmonised with Figma itself: Figma publishes its own colour variables into the plugin frame and updates them live, so the theming is pure CSS with zero JavaScript and switches with the host app. Two refinements were worth the time — the first dark palette read brown next to Figma’s neutral grey and was rebuilt neutral, and the flash of light theme on open was removed with a synchronous script that sets the class before the first paint.
A constraint that became the architecture
I am based in Ukraine, and for every payment platform the binding factor is the seller’s payout country. Lemon Squeezy, Stripe, Polar and Gumroad are all closed to it — and so is Figma’s own native plugin payment system. The answer was Paddle as merchant of record. Figma officially permits external payments, so the plugin lists as free and Figma takes 0% — better than the roughly 15% the native path would have cost. The constraint produced a better arrangement than the default would have.
Licensing followed the same logic: no database, no vendor, no recurring cost. The licence key is a signed token carrying the subscription id. Activation and validation verify the signature locally, then ask Paddle whether that subscription is still active. Refunds and cancellations therefore revoke access on their own, with no webhook to maintain, and it fails open by design — if the network is unreachable an existing Pro user stays Pro. Two bugs here were only findable by paying real money: wallet payments do not append the transaction id to the success URL, so the go-live purchase went through, the subscription activated and no key was issued; and purchase emails silently stopped sending because an API key was missing one read scope, producing a 403 on a lookup that only runs in the real purchase path. Pricing sits where the constraint left it — free for 12 imports a month with no account, Pro at $9 a month or $72 a year, against roughly $18 a month for the nearest competitor.
The launch was part of the build
The Figma Community listing was written for Community search — name, tagline, description and tags all keyword-tuned — with 5 carousel slides, a thumbnail, an icon and the required third-party payment disclosure. Around the product sits a 24-post blog, built as topical clusters around a pillar page with a sitemap, internal linking and structured data. The site also treats AI answers as a first-class channel: an llms.txt file of product facts for accurate citation, FAQ and software structured data, and a robots.txt that explicitly welcomes the AI crawlers instead of blocking them.
The keyword research changed the product’s own copy. “Editable layers, not a screenshot” reads like a differentiator, and it is not — every competitor in the category claims it. What is defensible is narrower and truer: measured from the real render, honest about what it cannot reproduce, no account required, and roughly a third of the price. The homepage was rewritten to lead with that.
Where it lives, and what is still open
Vellum is published on the Figma Community and payments are live, with real transactions processed end to end, keys issued and confirmation emails delivered. The last commit was 25 June 2026. Late in the project I deleted a working feature: Snapshot mode, a second pixel-based import pipeline, was removed to make the product simpler to understand, and multi-viewport import — which had only existed inside it — was rebuilt in the main Layers mode.
What is still open, stated plainly because the brand rule applies to the case study too: the rendering environment has no WebGL 2, so 3D scenes are filtered out by design rather than reproduced; two import-fidelity items are parked until there are clean examples of them; and there is dormant code left over from the deleted Snapshot mode.
← Alexey Sukhariev