| name | web-to-figma-chrome-extension |
| description | Chrome extension that captures webpages and converts them into editable Figma-compatible JSON files with support for full-page and element capture |
| triggers | ["capture webpage to figma format","convert html to figma json","export website design to figma","create figma file from webpage","capture web elements for figma","build chrome extension for web capture","parse webpage into figma data structure","extract design tokens from website"] |
Web to Figma Chrome Extension
Skill by ara.so — Design Skills collection
Overview
Web to Figma is a Chrome extension that captures any webpage and exports it as a Figma-compatible JSON file. It provides an in-page floating toolbar for one-click capture, supports cross-origin image proxy fetching to handle CORS issues, and offers configurable concurrency settings for image downloads.
Key capabilities:
- Full-page capture of DOM elements, styles, and layout
- Element-specific capture for targeted design extraction
- Cross-origin image proxy mode to avoid missing images
- Configurable image fetch concurrency (4/6/8/10/12/16/20/infinite)
- Export as
.json for Figma workflows
Installation
Developer Mode (Local)
- Clone the repository:
git clone https://github.com/Paidax01/web-to-figma.git
cd web-to-figma
-
Open Chrome and navigate to chrome://extensions/
-
Enable Developer mode (toggle in top-right)
-
Click Load unpacked
-
Select the web-to-figma directory
The extension icon should now appear in your Chrome toolbar.
Project Architecture
web-to-figma/
├── manifest.json # Extension configuration
├── background.js # Service worker for proxy and coordination
├── capture.js # Core capture logic (DOM traversal, style extraction)
├── runner.js # Orchestrates capture process
├── inpage-toolbar.js # Floating UI toolbar on webpage
├── popup.html/css/js # Extension popup UI
└── logo/ # Extension icons
Key Components
capture.js: Main capture engine that traverses the DOM, extracts computed styles, handles images, text nodes, and layout information
background.js: Background service worker that proxies cross-origin image requests
runner.js: Coordinates the capture flow and message passing between components
inpage-toolbar.js: Injects floating toolbar UI into webpages for quick access
Core Capture Flow
1. Extension Popup Configuration
The popup (popup.html) provides settings:
document.getElementById('startCapture').addEventListener('click', async () => {
const useProxy = document.getElementById('proxyMode').checked;
const concurrency = document.getElementById('concurrency').value;
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.tabs.sendMessage(tab.id, {
action: 'startCapture',
config: {
useProxy: useProxy,
imageConcurrency: parseInt(concurrency)
}
});
});
2. Triggering Capture
From the in-page toolbar or popup:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'startCapture') {
const config = message.config || {};
captureWebpage(config).then(result => {
downloadJSON(result, `figma-capture-${Date.now()}.json`);
});
}
});
3. DOM Traversal and Data Extraction
The capture engine recursively walks the DOM:
function captureElement(element, config) {
const computedStyle = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
const nodeData = {
type: element.tagName.toLowerCase(),
id: element.id || null,
className: element.className || null,
position: {
x: rect.left + window.scrollX,
y: rect.top + window.scrollY,
width: rect.width,
height: rect.height
},
styles: extractStyles(computedStyle),
text: extractText(element),
children: []
};
if (element.tagName === 'IMG') {
nodeData.src = element.src;
if (config.useProxy) {
nodeData.proxyUrl = await fetchImageViaProxy(element.src, config);
}
}
( child element.) {
((child)) {
nodeData..((child, config));
}
}
nodeData;
}
() {
{
: computedStyle.,
: computedStyle.,
: computedStyle.,
: computedStyle.,
: computedStyle.,
: computedStyle.,
: {
: computedStyle.,
: computedStyle.,
: computedStyle.,
: computedStyle.
},
: {
: computedStyle.,
: computedStyle.,
: computedStyle.,
: computedStyle.
},
: {
: computedStyle.,
: computedStyle.,
: computedStyle.,
: computedStyle.
},
: computedStyle.,
: computedStyle.,
: computedStyle.
};
}
4. Cross-Origin Image Handling
Background proxy pattern:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'fetchImage') {
fetch(message.url, {
method: 'GET',
mode: 'cors',
credentials: 'omit'
})
.then(response => response.blob())
.then(blob => {
const reader = new FileReader();
reader.onloadend = () => {
sendResponse({
success: true,
dataUrl: reader.result
});
};
reader.readAsDataURL(blob);
})
.catch(error => {
sendResponse({
success: false,
error: error.message
});
});
return true;
}
});
async function fetchImageViaProxy() {
( {
chrome..({
: ,
: url
}, {
(response.) {
(response.);
} {
( (response.));
}
});
});
}
5. Concurrency Control
Managing parallel image fetches:
class ConcurrencyQueue {
constructor(limit) {
this.limit = limit === 'infinite' ? Infinity : limit;
this.running = 0;
this.queue = [];
}
async add(fn) {
while (this.running >= this.limit) {
await new Promise(resolve => this.queue.push(resolve));
}
this.running++;
try {
return await fn();
} finally {
this.running--;
const resolve = this.queue.shift();
if (resolve) resolve();
}
}
}
async function captureImagesWithConcurrency(images, config) {
queue = (config. || );
.(
images.(
queue.( (img., config))
)
);
}
Configuration
manifest.json
{
"manifest_version": 3,
"name": "Web to Figma",
"version": "1.0.0",
"description": "Convert any webpage into an editable Figma file",
"permissions": [
"activeTab",
"storage",
"downloads"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["inpage-toolbar.js", "capture.js"
Runtime Configuration
Pass config object to capture functions:
const captureConfig = {
useProxy: true,
imageConcurrency: 8,
fullPage: true,
includeHidden: false,
maxDepth: 50,
captureIframes: false,
captureBackgrounds: true,
minimumSize: { width: 1, height: 1 }
};
Common Patterns
Full-Page Capture
async function captureFullPage() {
const config = {
useProxy: true,
imageConcurrency: 8,
fullPage: true
};
window.scrollTo(0, 0);
const rootElement = document.body;
const captureData = await captureElement(rootElement, config);
const result = {
version: '1.0',
timestamp: new Date().toISOString(),
url: window.location.href,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
document: {
width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight
},
: captureData
};
result;
}
Element-Specific Capture
function captureSpecificElement(selector) {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
const config = {
useProxy: true,
imageConcurrency: 6,
fullPage: false
};
return captureElement(element, config);
}
const headerData = await captureSpecificElement('header.main-header');
Download JSON Result
function downloadJSON(data, filename) {
const jsonString = JSON.stringify(data, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
Handling SVGs
function captureSVG(svgElement) {
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svgElement);
const svgDataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`;
return {
type: 'svg',
content: svgString,
dataUrl: svgDataUrl,
viewBox: svgElement.getAttribute('viewBox'),
width: svgElement.width.baseVal.value,
height: svgElement.height.baseVal.value
};
}
Canvas Capture
function captureCanvas(canvasElement) {
try {
const dataUrl = canvasElement.toDataURL('image/png');
return {
type: 'canvas',
dataUrl: dataUrl,
width: canvasElement.width,
height: canvasElement.height
};
} catch (error) {
return {
type: 'canvas',
error: 'Tainted canvas - cross-origin content',
width: canvasElement.width,
height: canvasElement.height
};
}
}
Advanced Usage
Custom Style Extraction
function extractCustomProperties(element) {
const styles = window.getComputedStyle(element);
const customProps = {};
for (let i = 0; i < styles.length; i++) {
const prop = styles[i];
if (prop.startsWith('--')) {
customProps[prop] = styles.getPropertyValue(prop);
}
}
return customProps;
}
Text Node Extraction
function extractTextNodes(element) {
const textNodes = [];
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (text) {
const range = document.createRange();
range.selectNode(node);
const rect = range.getBoundingClientRect();
textNodes.push({
text: text,
position: {
x: rect.left + window.scrollX,
y: rect.top + window.scrollY,
width: rect.width,
height: rect.height
}
});
}
}
return textNodes;
}
Background Image Extraction
function extractBackgroundImages(element) {
const style = window.getComputedStyle(element);
const bgImage = style.backgroundImage;
if (bgImage && bgImage !== 'none') {
const urlMatch = bgImage.match(/url\(['"]?(.*?)['"]?\)/);
if (urlMatch && urlMatch[1]) {
return {
url: urlMatch[1],
size: style.backgroundSize,
position: style.backgroundPosition,
repeat: style.backgroundRepeat
};
}
}
return null;
}
Progressive Capture with Progress Callback
async function captureWithProgress(element, config, onProgress) {
const totalElements = element.querySelectorAll('*').length;
let processedElements = 0;
async function captureWithCallback(el) {
processedElements++;
if (onProgress) {
onProgress({
processed: processedElements,
total: totalElements,
percentage: (processedElements / totalElements * 100).toFixed(2)
});
}
return captureElement(el, config);
}
return await captureWithCallback(element);
}
const result = await captureWithProgress(
document.body,
{ useProxy: true, imageConcurrency: 8 },
(progress) => {
console.log(`Capturing: ${progress.percentage}%`);
}
);
Troubleshooting
Images Not Capturing
Problem: Images appear as broken or missing in exported JSON.
Solutions:
- Enable cross-origin proxy mode:
const config = { useProxy: true, imageConcurrency: 8 };
- Verify background.js has proper permissions in manifest.json:
{
"host_permissions": ["<all_urls>"]
}
- Check if images are lazy-loaded:
async function scrollToLoadImages() {
const scrollStep = window.innerHeight;
const scrollMax = document.body.scrollHeight;
for (let y = 0; y < scrollMax; y += scrollStep) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 200));
}
window.scrollTo(0, 0);
}
await scrollToLoadImages();
const result = await captureFullPage();
Capture Timeout or Slow Performance
Problem: Capture takes too long or times out.
Solutions:
- Reduce image concurrency:
const config = { imageConcurrency: 4 };
- Skip hidden elements:
function shouldCaptureElement(element) {
const style = window.getComputedStyle(element);
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0';
}
- Limit DOM depth:
function captureElementWithDepth(element, config, currentDepth = 0) {
if (currentDepth > config.maxDepth) {
return null;
}
}
Extension Not Injecting
Problem: Toolbar or capture functionality not appearing on page.
Solutions:
- Check content script injection in manifest.json:
{
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["inpage-toolbar.js", "capture.js", "runner.js"],
"run_at": "document_idle"
}]
}
- Verify no CSP (Content Security Policy) blocks:
- Reload extension after code changes:
Memory Issues with Large Pages
Problem: Browser crashes or slows down on large/complex pages.
Solutions:
- Capture in chunks:
async function captureInChunks(rootElement, chunkSize = 100) {
const allElements = Array.from(rootElement.querySelectorAll('*'));
const chunks = [];
for (let i = 0; i < allElements.length; i += chunkSize) {
const chunk = allElements.slice(i, i + chunkSize);
chunks.push(await Promise.all(
chunk.map(el => captureElement(el, config))
));
await new Promise(resolve => setTimeout(resolve, 0));
}
return chunks.flat();
}
- Exclude large elements:
const config = {
minimumSize: { width: 10, height: 10 },
excludeSelectors: ['.ad-container', '.comments-section']
};
JSON Export Too Large
Problem: Generated JSON file is too large to download or process.
Solutions:
- Compress data:
function compressStyles(styles) {
const compressed = {};
const defaults = {
color: 'rgb(0, 0, 0)',
backgroundColor: 'rgba(0, 0, 0, 0)',
};
for (const [key, value] of Object.entries(styles)) {
if (value !== defaults[key]) {
compressed[key] = value;
}
}
return compressed;
}
- Paginate output:
function exportInPages(captureData, elementsPerFile = 500) {
const files = [];
const elements = flattenTree(captureData);
for (let i = 0; i < elements.length; i += elementsPerFile) {
const chunk = elements.slice(i, i + elementsPerFile);
files.push({
filename: `figma-capture-page-${Math.floor(i / elementsPerFile) + 1}.json`,
data: { elements: chunk }
});
}
return files;
}
Packaging for Distribution
Create Distribution Build
zip -r web-to-figma-extension.zip . \
-x "*.DS_Store" \
-x ".git/*" \
-x "node_modules/*" \
-x "*.md" \
-x "tests/*"
Version Management
Update version in manifest.json:
{
"version": "1.0.1",
"version_name": "1.0.1 Beta"
}
Integration with Figma API
While this extension exports JSON, you can process it for Figma import:
function convertToFigmaNodes(captureData) {
return {
name: captureData.type || 'Frame',
type: mapToFigmaType(captureData.type),
x: captureData.position.x,
y: captureData.position.y,
width: captureData.position.width,
height: captureData.position.height,
fills: convertFills(captureData.styles.backgroundColor),
strokes: convertStrokes(captureData.styles.border),
children: captureData.children.map(convertToFigmaNodes)
};
}
function mapToFigmaType(htmlType) {
const typeMap = {
'div': 'FRAME',
'span': 'TEXT',
'img': 'RECTANGLE',
'svg': 'VECTOR'
};
return typeMap[htmlType] || 'FRAME';
}
Best Practices
- Always test on sample pages first before capturing production sites
- Use proxy mode for public websites with image CDNs
- Adjust concurrency based on network speed (slower connection = lower concurrency)
- Clear browser cache if getting stale captures
- Respect robots.txt and terms of service when capturing third-party sites
- Handle errors gracefully - not all pages will capture perfectly
- Version your capture format for backward compatibility
Legal and Ethical Considerations
- Only capture content you have permission to use
- Respect copyright and intellectual property rights
- Follow website terms of service
- Do not capture sensitive or personal information without consent
- Comply with GDPR, CCPA, and other privacy regulations
- Use for learning, research, or authorized design workflows only