| name | figma-capture-extension |
| description | Chrome extension that captures webpages into Figma's clipboard format with font fixes and DOM cleanup |
| triggers | ["capture a webpage to Figma","install figma capture extension","fix CJK fonts when capturing to Figma","customize font mapping for Figma capture","debug Figma capture clipboard issues","configure figma-capture chrome extension","flatten DOM for Figma import","bypass CSP when capturing to Figma"] |
Figma Capture Extension
Skill by ara.so — Design Skills collection
This Chrome extension captures any webpage into Figma's clipboard format, adding post-processing to fix fonts (especially CJK), clean up DOM structure, and remove empty elements before pasting into Figma.
What It Does
- CJK Font Correction: Detects Chinese/Japanese/Korean text and remaps to
PingFang SC / Noto Serif SC
- Font Mapping: Remaps unavailable fonts via configurable
font-map.json
- Default Font Fallback: Assigns
Noto Sans SC to elements without explicit fonts
- DOM Flattening: Removes wrapper elements that don't contribute visually
- Empty Frame Cleanup: Strips zero-size elements and childless containers
- Event Isolation: Prevents toolbar clicks from affecting host page behavior
Installation
Step 1: Download Figma's Capture Script
make
This downloads capture.js from Figma's public endpoint. The Makefile does:
capture.js:
curl -o capture.js https://www.figma.com/community/plugin/1159123024924461424/capture.js
Step 2: Configure Font Mapping
cp font-map.example.json font-map.json
Example font-map.json structure:
{
"Arial": "Inter",
"Helvetica": "Inter",
"SF Pro Display": "Google Sans Flex",
"Roboto": "Noto Sans SC",
"system-ui": "Inter"
}
Step 3: Load Extension in Chrome
- Navigate to
chrome://extensions
- Enable Developer mode (top right toggle)
- Click Load unpacked
- Select the
figma-capture directory
Usage
Basic Capture Workflow
- Navigate to the webpage you want to capture
- Click the extension icon or press
Alt+Shift+F
- Use the toolbar to:
- Capture entire page
- Select specific element by clicking
- Switch to Figma
- Press
Ctrl+V (Windows) or Cmd+V (Mac)
Keyboard Shortcut
Default: Alt+Shift+F
To customize, go to chrome://extensions/shortcuts
Configuration Files
manifest.json
Key configuration sections:
{
"manifest_version": 3,
"name": "Figma Capture",
"version": "1.0.0",
"permissions": [
"activeTab",
"clipboardWrite"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"action": {
"default_icon": "icon.png"
Font Mapping Configuration
Edit font-map.json to add custom font substitutions:
{
"SegoeUI": "Inter",
"San Francisco": "Google Sans Flex",
"PingFang": "PingFang SC",
"Microsoft YaHei": "Noto Sans SC",
"Hiragino Sans": "Noto Sans JP"
}
Font mapping logic (applied in order):
- CJK detection →
PingFang SC / Noto Serif SC
- Custom
font-map.json mappings
- Icon font detection → preserve original
- No font specified →
Noto Sans SC
Architecture
Component Flow
User clicks extension icon
↓
background.js activates content script
↓
content.js injects capture.js
↓
capture.js serializes DOM → clipboard payload
↓
Clipboard interceptor transforms payload:
- Font correction
- DOM cleanup
- Wrapper flattening
↓
Modified payload written to clipboard
↓
User pastes into Figma
Key Files
- background.js: Service worker, patches
attachShadow for event isolation
- content.js: Injected script, intercepts clipboard API
- capture.js: Figma's official serializer (downloaded, not in repo)
- font-map.json: User-configurable font substitutions
Code Examples
Clipboard Interception Pattern
const originalWrite = navigator.clipboard.write;
navigator.clipboard.write = async function(data) {
const items = await Promise.all(data.map(async item => {
if (item.types.includes('text/html')) {
const blob = await item.getType('text/html');
const html = await blob.text();
const transformed = transformFigmaPayload(html);
return new ClipboardItem({
'text/html': new Blob([transformed], { type: 'text/html' })
});
}
return item;
}));
return originalWrite.call(this, items);
};
Font Detection and Remapping
function detectAndFixFont(element, computedStyle) {
const text = element.textContent || '';
const fontFamily = computedStyle.fontFamily;
const cjkRegex = /[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/;
if (cjkRegex.test(text)) {
const isSerif = /serif/i.test(fontFamily);
return isSerif ? 'Noto Serif SC' : 'PingFang SC';
}
const fontMap = loadFontMap();
for (const [original, replacement] of Object.entries(fontMap)) {
if (fontFamily.includes(original)) {
return replacement;
}
}
if (/icon|symbol|awesome|material/i.test(fontFamily)) {
return fontFamily;
}
return fontFamily || 'Noto Sans SC';
}
DOM Cleanup: Wrapper Flattening
function flattenWrappers(node) {
if (node.children.length !== 1) return false;
const parent = node;
const child = node.children[0];
const parentStyle = getComputedStyle(parent);
const childStyle = getComputedStyle(child);
const isPassThrough =
parentStyle.backgroundColor === 'transparent' &&
parentStyle.border === 'none' &&
parent.getBoundingClientRect().width === child.getBoundingClientRect().width &&
parent.getBoundingClientRect().height === child.getBoundingClientRect().height;
if (isPassThrough) {
parent.replaceWith(child);
return true;
}
return false;
}
Event Isolation for Toolbar
const shadowRoots = new WeakMap();
const originalAttachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function(init) {
const shadowRoot = originalAttachShadow.call(this, init);
if (init.mode === 'closed') {
shadowRoots.set(this, shadowRoot);
}
return shadowRoot;
};
window.addEventListener('click', (event) => {
const toolbar = document.querySelector('.figma-capture-toolbar');
if (!toolbar) return;
const toolbarHost = toolbar.host;
const shadowRoot = shadowRoots.get(toolbarHost);
if (shadowRoot) {
event.stopPropagation();
event.preventDefault();
const point = shadowRoot.elementFromPoint(event.clientX, event.);
(point) {
point.( (, event));
}
}
}, );
Common Patterns
Handling Special Elements
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'LINK', 'META'];
function shouldProcessElement(element) {
if (SKIP_TAGS.includes(element.tagName)) {
return false;
}
const style = getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden') {
return false;
}
const rect = element.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) {
return false;
}
return true;
}
Custom Font Mapping for Specific Websites
const siteSpecificMaps = {
'github.com': {
'-apple-system': 'Inter',
'BlinkMacSystemFont': 'Inter',
'Segoe UI': 'Inter'
},
'twitter.com': {
'TwitterChirp': 'Google Sans Flex',
'Helvetica Neue': 'Inter'
}
};
function getFontMap() {
const hostname = window.location.hostname;
const baseMap = JSON.parse(localStorage.getItem('font-map.json') || '{}');
const siteMap = siteSpecificMaps[hostname] || {};
return { ...baseMap, ...siteMap };
}
Troubleshooting
Fonts Not Displaying Correctly in Figma
Problem: Pasted elements show default Times or incorrect fonts
Solution: Check if fonts are installed in Figma:
console.log('Applied font:', appliedFont);
console.log('Original font:', originalFont);
console.log('Font map:', fontMap);
Update font-map.json to map problematic fonts to fonts you have in Figma:
{
"ProblematicFont": "Inter",
"AnotherBadFont": "Roboto"
}
Extension Not Activating
Problem: Clicking icon does nothing
Solution: Check console for errors:
- Right-click extension icon → Inspect popup
- Go to
chrome://extensions → Find Figma Capture → Click "background page"
- Check for errors in console
Verify capture.js exists:
ls -la capture.js
make
Clipboard Payload Not Modified
Problem: Fonts/cleanup not being applied
Solution: Verify clipboard interceptor is loaded:
console.log(navigator.clipboard.write.toString());
Reload extension:
chrome://extensions
- Click reload icon for Figma Capture
- Refresh target webpage
Empty or Broken Capture
Problem: Pasting creates empty frame or errors
Solution: Some elements may be over-cleaned. Adjust cleanup logic:
const ENABLE_FLATTENING = false;
if (ENABLE_FLATTENING) {
flattenWrappers(node);
}
Check for CSP violations in console — some sites block extension scripts.
CJK Text Not Detected
Problem: Chinese/Japanese/Korean text uses wrong font
Solution: Verify regex pattern matches your text:
const text = "你好世界";
const cjkRegex = /[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/;
console.log(cjkRegex.test(text));
Manually set font in font-map.json:
{
"SimSun": "Noto Sans SC",
"Microsoft YaHei": "PingFang SC"
}
Advanced Configuration
Modify Default Fallback Font
Edit content.js:
const DEFAULT_FALLBACK_FONT = 'Inter';
Add Custom Cleanup Rules
function customCleanup(element) {
for (const attr of element.attributes) {
if (attr.name.startsWith('data-')) {
element.removeAttribute(attr.name);
}
}
const REMOVE_CLASSES = ['ad', 'banner', 'cookie-notice'];
element.classList.remove(...REMOVE_CLASSES);
}
Debug Mode
Add to content.js:
const DEBUG = true;
function debug(...args) {
if (DEBUG) {
console.log('[Figma Capture]', ...args);
}
}
debug('Font applied:', fontFamily);
debug('Element cleaned:', element.tagName);
Limitations
- Figma Dependency: Relies on Figma's undocumented clipboard format (may break)
- Font Availability: Target fonts must be installed in Figma
- CSP Restrictions: Some sites block extension script injection
- Dynamic Content: May not capture lazy-loaded or JavaScript-rendered elements
capture.js Updates: Figma may change/remove the download endpoint
Resources