@magicpatterns/html-to-figma
v1.0.4
Published
Convert a live HTML subtree into a Figma clipboard payload that pastes losslessly into Figma.
Keywords
Readme
@magicpatterns/html-to-figma
Converts a live DOM subtree into the binary clipboard payload Figma reads on
paste. Runs in the browser — you hand it the element you want exported, it
returns a text/html string you can write to the system clipboard, and
pasting into Figma materialises the tree as real Figma nodes (frames,
text, vectors, images).
How it works
Two-step flow:
inlineComputedStyles(root)— must run in a browser with the element mounted. Walks the subtree, reads computed styles from the live layout, stamps each element's inlinestyleattribute, and records its rendered rect asdata-rect="x,y,w,h". Returns the serialised HTML string. This snapshot is what the next step consumes — it doesn't need the live layout engine anymore.generateFromElement(cloneRoot, { topLayerName })— parses the inlined clone, preloads referenced fonts and images in parallel, builds a Figma node tree, encodes thefig-kiwibinary envelope, and returns thetext/htmlstring. Write it to the clipboard under thetext/htmlMIME:const html = await generateFromElement(root, { topLayerName: 'MyExport' }) await navigator.clipboard.write([ new ClipboardItem({ 'text/html': new Blob([html], { type: 'text/html' }) }), ])
Pressing ⌘V in Figma produces the node tree.
Install & use
yarn add @magicpatterns/html-to-figmaNew API (preferred)
import {
generateFromElement,
inlineComputedStyles,
} from '@magicpatterns/html-to-figma'
const root = document.getElementById('to-export')!
// Snapshot computed styles + rects from the live DOM.
const inlined = inlineComputedStyles(root)
// Reparse so the generator works on a clean, detached DOM.
const clone = new DOMParser()
.parseFromString(inlined, 'text/html')
.body.firstElementChild!
const payload = await generateFromElement(clone, {
topLayerName: 'MyExport',
})
await navigator.clipboard.write([
new ClipboardItem({ 'text/html': new Blob([payload], { type: 'text/html' }) }),
])Exporting multiple screens at once
When a canvas-style UI wants to paste several designs into Figma with
their relative positions preserved, call generateFromElements instead
of combining payloads by hand. It composes one clipboard archive
with a single wrapper FRAME containing each screen at its given {x,
y}. Fonts, images, and vector blobs are de-duplicated across screens.
import {
generateFromElements,
inlineComputedStyles,
} from '@magicpatterns/html-to-figma'
// Ask each iframe / node for its inlined HTML, parse each back to an
// Element, then collect into a ScreenSpec[].
const screens = selectedNodes.map((node) => {
const inlined = inlineComputedStyles(node.exportRoot)
const root = new DOMParser()
.parseFromString(inlined, 'text/html')
.body.firstElementChild!
return { root, x: node.x - minX, y: node.y - minY, name: node.name }
})
const payload = await generateFromElements(screens, { name: 'My Canvas' })
await navigator.clipboard.write([
new ClipboardItem({ 'text/html': new Blob([payload], { type: 'text/html' }) }),
])Legacy API (for migration)
The original htmlToFigma export is still available and returns the
same intermediate JSON tree it always has. It's maintained for
backwards-compatibility while callers migrate to generateFromElement.
New code should use the new API.
import { htmlToFigma } from '@magicpatterns/html-to-figma'
const tree = htmlToFigma(document.getElementById('to-export')!)
// ... existing consumer code handles `tree`Public API surface
| Export | Kind | Purpose |
|---|---|---|
| generateFromElement | async fn | Preferred. Returns the Figma clipboard text/html payload for one DOM subtree. |
| generateFromElements | async fn | Multi-screen variant: composes N subtrees into one payload with each at its given {x, y}. |
| inlineComputedStyles | fn | Snapshot live-DOM computed styles + rects into an HTML string. |
| GenerateOptions | type | Options shape for generateFromElement. |
| GenerateManyOptions | type | Options shape for generateFromElements. |
| ScreenSpec | type | Shape of each screen passed to generateFromElements. |
| htmlToFigma | fn | Legacy. Synchronous intermediate-JSON export. |
Development
Running the playground
The repo ships a Vite app at src/dev/ with example components and a
"Copy for Figma paste" button. Use it to try out changes end-to-end
before shipping — ⌘V into Figma after copying.
yarn dev # starts Vite on http://localhost:5173Drop new example components into src/dev/ExampleComponents/ and wire
them into src/dev/App.tsx's EXAMPLES array.
Building the library
yarn build # vite build → dist/htmlToFigma.{js,umd.cjs}
# then tsc -p tsconfig.build.json → dist/*.d.ts
yarn preview # serves the built bundles for smoke-testingvite.config.ts marks opentype.js, kiwi-schema, and pako as
external — the consumer's bundler resolves them from node_modules so
we don't duplicate runtime deps.
Linting
yarn lintDebugging scripts
Node-only scripts under scripts/ help inspect the binary format:
| Script | What it does |
|---|---|
| yarn generate-fixture <slug> | Reads fixtures/<slug>.input.html, runs the generator, writes fixtures/<slug>.generated.html. pbcopy < fixtures/<slug>.generated.html to paste-test. |
| yarn decode-fixture <slug> | Parses an archive and dumps the JSON node tree. Great for seeing what the canonical Figma output produces. |
| yarn decode-blob <slug> <n> | Decodes a single glyph/vector blob out of an archive's blob table. |
| yarn extract-schema <slug> | Pulls the embedded kiwi schema from a fixture and rewrites src/lib/figmaSchema.ts. Only needed when Figma bumps the clipboard schema. |
| yarn roundtrip-fixture <slug> | Decodes and re-encodes a fixture, verifying our archive reader/writer stays byte-reversible. |
| yarn test-fixture <slug> | Runs the generator against a fixture's saved input HTML. |
| yarn verify-generated <slug> | Sanity-checks a generated payload parses and has no obvious schema violations. |
Project layout
src/lib/ ★ everything that ships
src/dev/ ✗ Vite playground (never published)
fixtures/ ✗ captured input + reference HTML pairs (never published)
scripts/ ✗ debug utilities (never published)
assets/ ✗ bundled Inter TTFs for Node-side tests (never published)
.reference/ ✗ committed reference material, not published
dist/ ★ build output (gitignored, generated by `yarn build`)Three layers keep non-ship files out of published tarballs:
package.json"files": ["dist"]— npm only publishes allowlisted entries. Everything else is already invisible tonpm publish..npmignore— explicit deny list as a second pass.prepublishOnlyscript — runsyarn buildand asserts.reference/did not leak intodist/before publish proceeds.
Verify what would ship with npm pack --dry-run.
Publishing
TL;DR
# from packages/html-to-figma/
# 1. bump the version
npm version patch # or: minor | major | <exact-version>
# 2. preview what will ship
npm pack --dry-run
# 3. publish
npm publish --access restrictedThat's it. You do NOT need to run yarn build manually — the
prepublishOnly hook runs it automatically and verifies the output
before anything hits the registry.
Step-by-step
Make sure main is clean and
yarnis up to date.git status # nothing uncommitted yarn install # if package.json dependencies changed recentlyBump the version.
npm versioneditspackage.jsonand creates a version-bump commit + git tag in one go.npm version patch # 0.0.36 → 0.0.37 # npm version minor # 0.0.36 → 0.1.0 # npm version major # 0.0.36 → 1.0.0 # npm version 0.1.2 # exactPreview the tarball contents. Confirms only
dist/+README.mdpackage.jsonship — nofixtures/, no.reference/, noscripts/.
npm pack --dry-runPublish.
npm publish --access restrictedWhat happens under the hood when you run
publish:- npm fires the
prepublishOnlyhook, which runs:yarn build(clean rebuild ofdist/)- a sanity check that
dist/htmlToFigma.jsexists - a sanity check that
dist/.reference/did not leak in
- npm packs the allowlisted files (
"files": ["dist"]inpackage.json, plusREADME.md+package.jsonby default). - npm uploads the tarball to the registry.
If
prepublishOnlythrows, fix the underlying issue and retry — don'tnpm publish --forcepast it. The checks exist to prevent broken or leaky releases.- npm fires the
Push the version commit + tag.
git push --follow-tags
Reference material
.reference/ contains committed-but-unpublished source from the
upstream Figma export tool we reverse-engineered. Read the files there
(especially the chunk-*.pretty.js extracts) when you need to
understand a behaviour the canonical exporter produces. See
.reference/README.md.
