
web-to-figma-chrome-extension
Chrome extension that captures webpages and converts them into editable Figma-compatible JSON files with support for full-page and element capture
- Installs
- 128
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/reason-machines/design-skills --skill web-to-figma-chrome-extension- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads web-to-figma-chrome-extension automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
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
.jsonfor 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-figmadirectory
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 informationbackground.js: Background service worker that proxies cross-origin image requestsrunner.js: Coordinates the capture flow and message passing between componentsinpage-toolbar.js: Injects floating toolbar UI into webpages for quick access
Core Capture Flow
1. Extension Popup Configuration
The popup (popup.html) provides settings:
// Example popup.js structure
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:
// inpage-toolbar.js pattern
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'startCapture') {
const config = message.config || {};
// Start capture process
captureWebpage(config).then(result => {
// Generate download
downloadJSON(result, `figma-capture-${Date.now()}.json`);
});
}
});
3. DOM Traversal and Data Extraction
The capture engine recursively walks the DOM:
// capture.js core pattern
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: []
};
// Handle images
if (element.tagName === 'IMG') {
nodeData.src = element.src;
if (config.useProxy) {
nodeData.proxyUrl = await fetchImageViaProxy(element.src, config);
}
}
// Recursively capture children
for (const child of element.children) {
if (shouldCaptureElement(child)) {
nodeData.children.push(captureElement(child, config));
}
}
return nodeData;
}
function extractStyles(computedStyle) {
return {
color: computedStyle.color,
backgroundColor: computedStyle.backgroundColor,
fontSize: computedStyle.fontSize,
fontFamily: computedStyle.fontFamily,
fontWeight: computedStyle.fontWeight,
lineHeight: computedStyle.lineHeight,
padding: {
top: computedStyle.paddingTop,
right: computedStyle.paddingRight,
bottom: computedStyle.paddingBottom,
left: computedStyle.paddingLeft
},
margin: {
top: computedStyle.marginTop,
right: computedStyle.marginRight,
bottom: computedStyle.marginBottom,
left: computedStyle.marginLeft
},
border: {
width: computedStyle.borderWidth,
style: computedStyle.borderStyle,
color: computedStyle.borderColor,
radius: computedStyle.borderRadius
},
display: computedStyle.display,
position: computedStyle.position,
zIndex: computedStyle.zIndex
};
}
4. Cross-Origin Image Handling
Background proxy pattern:
// background.js proxy implementation
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 response
}
});
// capture.js usage
async function fetchImageViaProxy(url, config) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
action: 'fetchImage',
url: url
}, response => {
if (response.success) {
resolve(response.dataUrl);
} else {
reject(new Error(response.error));
}
});
});
}
5. Concurrency Control
Managing parallel image fetches:
// Concurrency limiter pattern
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();
}
}
}
// Usage in capture
async function captureImagesWithConcurrency(images, config) {
const queue = new ConcurrencyQueue(config.imageConcurrency || 4);
return Promise.all(
images.map(img =>
queue.add(() => fetchImageViaProxy(img.src, 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", "runner.js"],
"run_at": "document_idle"
}
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "logo/icon16.png",
"48": "logo/icon48.png",
"128": "logo/icon128.png"
}
}
}
Runtime Configuration
Pass config object to capture functions:
const captureConfig = {
useProxy: true, // Enable cross-origin image proxy
imageConcurrency: 8, // Parallel image fetches (4/6/8/10/12/16/20/'infinite')
fullPage: true, // Capture entire page vs. viewport
includeHidden: false, // Capture elements with display:none
maxDepth: 50, // Maximum DOM traversal depth
captureIframes: false, // Whether to capture iframe content
captureBackgrounds: true, // Include CSS background images
minimumSize: { width: 1, height: 1 } // Skip tiny elements
};
Common Patterns
Full-Page Capture
async function captureFullPage() {
const config = {
useProxy: true,
imageConcurrency: 8,
fullPage: true
};
// Scroll to top
window.scrollTo(0, 0);
// Capture from root
const rootElement = document.body;
const captureData = await captureElement(rootElement, config);
// Add metadata
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
},
tree: captureData
};
return 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);
}
// Usage
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();
// Cleanup
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) {
// Canvas may be tainted by cross-origin content
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 = {};
// Extract CSS custom properties (variables)
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);
}
// Usage
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:
// Scroll through page to trigger lazy loading
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 }; // Lower value
- 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;
}
// ... capture logic
}
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:
// Check console for CSP errors
// May need to adjust for strict CSP pages
- Reload extension after code changes:
# In chrome://extensions/, click reload icon
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))
));
// Allow event loop to process
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) {
// Only include non-default values
const compressed = {};
const defaults = {
color: 'rgb(0, 0, 0)',
backgroundColor: 'rgba(0, 0, 0, 0)',
// ... other defaults
};
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
# Remove development files
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:
// Example post-processing for Figma plugin
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', // With image fill
'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
FAQ
What does the web-to-figma-chrome-extension skill do?
Chrome extension that captures webpages and converts them into editable Figma-compatible JSON files with support for full-page and element capture
How do I install web-to-figma-chrome-extension?
Run npx skills add https://github.com/reason-machines/design-skills --skill web-to-figma-chrome-extension in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/web-to-figma-chrome-extension/ (personal) or .claude/skills/web-to-figma-chrome-extension/ (project).
Which AI agents can use web-to-figma-chrome-extension?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is web-to-figma-chrome-extension safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains web-to-figma-chrome-extension?
It is published by reason-machines in reason-machines/design-skills. It has 128 recorded installs.
More from reason-machines/design-skills
awesome-claude-design
reason-machines/design-skills
Open the skill to read its SKILL.md instructions.
design-md-format
reason-machines/design-skills
Create and validate DESIGN.md files that give AI coding agents structured understanding of design systems through machine-readable tokens and human-readable rationale.
open-codesign-ai-design
reason-machines/design-skills
Use Open CoDesign to generate prototypes, slides, and PDFs from prompts with Claude, GPT, Gemini, or local models
open-design-ai-prototyping
reason-machines/design-skills
Open the skill to read its SKILL.md instructions.
keychron-hardware-design
reason-machines/design-skills
Access and work with Keychron keyboard and mouse industrial design files (STEP, DXF, DWG, PDF) for 135+ models to create compatible accessories, remixes, and modifications.
design-md-chrome-extractor
reason-machines/design-skills
Open the skill to read its SKILL.md instructions.
Popular frontend & design skills
setup-matt-pocock-skills
mattpocock/skills
Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
frontend-design
anthropics/skills
Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.
hyperframes-animation
heygen-com/hyperframes
All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.
prototype
mattpocock/skills
Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.
hyperframes-core
heygen-com/hyperframes
The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.
vercel-react-best-practices
vercel-labs/agent-skills
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.