html.to.design Electron SDK
Last updated
The html.to.design Electron SDK, @divriots/h2d-electron-sdk, captures whatever is rendered in your Electron app’s windows into an html.to.design .h2d payload. Drop the file into the html.to.design Figma plugin to get editable Figma layers of your app’s UI, with no browser extension needed.
Dev builds only by default.
mountCapturedoes nothing in packaged production builds unless you opt in withenabledIn.
Installation
pnpm add -D @divriots/h2d-electron-sdk
# or
npm install --save-dev @divriots/h2d-electron-sdk
# or
yarn add -D @divriots/h2d-electron-sdk
electron (v22 or later) is a peer dependency.
Quick start
Call mountCapture once in your main process, then call captureWebContents from your own menu item, keyboard shortcut or button. The SDK adds no UI of its own.
// In your Electron main process
import { app, BrowserWindow, dialog } from 'electron';
import { writeFile } from 'fs/promises';
import { mountCapture, captureWebContents } from '@divriots/h2d-electron-sdk';
// Once, after `app` is available: registers the capture preload
mountCapture(app);
// From your own trigger
async function captureToFile(win: BrowserWindow): Promise<void> {
const { bytes, filename } = await captureWebContents(win.webContents);
const { filePath } = await dialog.showSaveDialog(win, {
defaultPath: `${filename}.h2d`,
});
if (filePath) await writeFile(filePath, Buffer.from(bytes));
}
Then drag the .h2d file into the html.to.design plugin in Figma. To get Figma-pasteable content instead, send the bytes to the ‹div›RIOTS public API from your own code.
API reference
mountCapture(app, options?)
Registers the renderer preload in the default session. Call it once, after app is available.
| Option | Type | Default | Description |
|---|---|---|---|
enabledIn | (app) => boolean | dev only | Opt capture into packaged production builds. By default mountCapture no-ops when app.isPackaged. |
captureWebContents(webContents, options?)
Captures a webContents and resolves with the .h2d payload. mountCapture must have been called first.
| Option | Type | Default | Description |
|---|---|---|---|
url | string | wc.getURL() | Override the URL recorded in the capture. |
Returns:
| Field | Type | Description |
|---|---|---|
bytes | Uint8Array | The .h2d payload. |
filename | string | Suggested file name, with no extension. |
isCaptureInFlight()
Returns true while a capture is running, for example to disable your capture button.
Renderer requirements
None. The SDK works with Electron’s default webPreferences, and with sandbox: true and contextIsolation: true.
Security model
The capture runs in the renderer’s sandboxed isolated world, the same context as the preload: no Node and no file system. It reaches the main process only through a few IPC channels for what only the main process can do (the debugger protocol, cross-origin iframes and frame enumeration). The main-process code is shipped un-obfuscated, so you can read all of it.
The SDK sends nothing to html.to.design or any other server. Its only network requests load the fonts and images your page already uses, from the page itself, to embed them in the .h2d file. What happens to that file is up to your code.
Known limitations
- Cross-origin iframes are captured, with a few details approximated; one nested inside another cross-origin iframe is dropped.
- Closed shadow DOM is captured as opaque. Open shadow roots are captured.
Troubleshooting
TypeError: Cannot read properties of undefined (reading 'on')
Your shell has ELECTRON_RUN_AS_NODE=1 set, which runs the Electron binary as plain Node, without Electron APIs. Unset it:
unset ELECTRON_RUN_AS_NODE
Frequently asked questions
Does the Electron SDK send my app's content anywhere?
No. The capture runs locally in the renderer and sends nothing to html.to.design or any other server. Its only requests load the fonts and images your page already uses, to embed them in the .h2d file. You decide what to do with the .h2d bytes it returns.
Can I use the SDK in a packaged production build?
Only if you opt in. By default mountCapture does nothing when app.isPackaged is true; pass an enabledIn function to turn it on.