Chrome extension that captures webpages into Figma's clipboard format with font fixes and DOM cleanup
Scanned 9/8/2026
Install to Claude Code
npx -y skills add Aradotso/design-skills --skill figma-capture-extension --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Figma Capture Extension?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/aradotso-figma-capture-extension)More formats (shields.io, HTML) on the badges page.
---
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](https://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
```bash
make
```
This downloads `capture.js` from Figma's public endpoint. The Makefile does:
```makefile
capture.js:
curl -o capture.js https://www.figma.com/community/plugin/1159123024924461424/capture.js
```
### Step 2: Configure Font Mapping
```bash
cp font-map.example.json font-map.json
```
Example `font-map.json` structure:
```json
{
"Arial": "Inter",
"Helvetica": "Inter",
"SF Pro Display": "Google Sans Flex",
"Roboto": "Noto Sans SC",
"system-ui": "Inter"
}
```
### Step 3: Load Extension in Chrome
1. Navigate to `chrome://extensions`
2. Enable **Developer mode** (top right toggle)
3. Click **Load unpacked**
4. Select the `figma-capture` directory
## Usage
### Basic Capture Workflow
1. Navigate to the webpage you want to capture
2. Click the extension icon **or** press `Alt+Shift+F`
3. Use the toolbar to:
- Capture entire page
- Select specific element by clicking
4. Switch to Figma
5. 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:
```json
{
"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"
},
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Alt+Shift+F"
}
}
}
}
```
### Font Mapping Configuration
Edit `font-map.json` to add custom font substitutions:
```json
{
"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):
1. CJK detection → `PingFang SC` / `Noto Serif SC`
2. Custom `font-map.json` mappings
3. Icon font detection → preserve original
4. 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
```javascript
// Override clipboard write to transform Figma payload
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();
// Transform Figma's clipboard payload
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
```javascript
function detectAndFixFont(element, computedStyle) {
const text = element.textContent || '';
const fontFamily = computedStyle.fontFamily;
// CJK detection
const cjkRegex = /[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/;
if (cjkRegex.test(text)) {
// Check if serif
const isSerif = /serif/i.test(fontFamily);
return isSerif ? 'Noto Serif SC' : 'PingFang SC';
}
// Load font map
const fontMap = loadFontMap(); // from font-map.json
// Check for mapped font
for (const [original, replacement] of Object.entries(fontMap)) {
if (fontFamily.includes(original)) {
return replacement;
}
}
// Icon font detection (preserve)
if (/icon|symbol|awesome|material/i.test(fontFamily)) {
return fontFamily;
}
// Default fallback
return fontFamily || 'Noto Sans SC';
}
```
### DOM Cleanup: Wrapper Flattening
```javascript
function flattenWrappers(node) {
// Skip if not a wrapper candidate
if (node.children.length !== 1) return false;
const parent = node;
const child = node.children[0];
// Check if parent is just a pass-through container
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) {
// Promote child to parent's position
parent.replaceWith(child);
return true;
}
return false;
}
```
### Event Isolation for Toolbar
```javascript
// In background.js - save closed shadow roots
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;
};
// In content.js - intercept toolbar clicks
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();
// Re-dispatch inside shadow DOM
const point = shadowRoot.elementFromPoint(event.clientX, event.clientY);
if (point) {
point.dispatchEvent(new MouseEvent('click', event));
}
}
}, true); // Capture phase
```
## Common Patterns
### Handling Special Elements
```javascript
// Skip script/style/svg from processing
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'LINK', 'META'];
function shouldProcessElement(element) {
if (SKIP_TAGS.includes(element.tagName)) {
return false;
}
// Skip hidden elements
const style = getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden') {
return false;
}
// Skip zero-size elements
const rect = element.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) {
return false;
}
return true;
}
```
### Custom Font Mapping for Specific Websites
```javascript
// Create site-specific font mappings
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:
```javascript
// Add debug logging to see what fonts are being applied
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:
```json
{
"ProblematicFont": "Inter",
"AnotherBadFont": "Roboto"
}
```
### Extension Not Activating
**Problem**: Clicking icon does nothing
**Solution**: Check console for errors:
1. Right-click extension icon → Inspect popup
2. Go to `chrome://extensions` → Find Figma Capture → Click "background page"
3. Check for errors in console
Verify `capture.js` exists:
```bash
ls -la capture.js
# If missing:
make
```
### Clipboard Payload Not Modified
**Problem**: Fonts/cleanup not being applied
**Solution**: Verify clipboard interceptor is loaded:
```javascript
// In browser console on target page:
console.log(navigator.clipboard.write.toString());
// Should show wrapped function, not native code
```
Reload extension:
1. `chrome://extensions`
2. Click reload icon for Figma Capture
3. Refresh target webpage
### Empty or Broken Capture
**Problem**: Pasting creates empty frame or errors
**Solution**: Some elements may be over-cleaned. Adjust cleanup logic:
```javascript
// Disable wrapper flattening temporarily
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:
```javascript
const text = "你好世界";
const cjkRegex = /[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/;
console.log(cjkRegex.test(text)); // Should be true
```
Manually set font in `font-map.json`:
```json
{
"SimSun": "Noto Sans SC",
"Microsoft YaHei": "PingFang SC"
}
```
## Advanced Configuration
### Modify Default Fallback Font
Edit `content.js`:
```javascript
const DEFAULT_FALLBACK_FONT = 'Inter'; // Change from 'Noto Sans SC'
```
### Add Custom Cleanup Rules
```javascript
function customCleanup(element) {
// Remove all data attributes
for (const attr of element.attributes) {
if (attr.name.startsWith('data-')) {
element.removeAttribute(attr.name);
}
}
// Remove specific classes
const REMOVE_CLASSES = ['ad', 'banner', 'cookie-notice'];
element.classList.remove(...REMOVE_CLASSES);
}
```
### Debug Mode
Add to `content.js`:
```javascript
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
- Figma HTML to Design Plugin: https://www.figma.com/community/plugin/1159123024924461424
- Chrome Extension Manifest V3: https://developer.chrome.com/docs/extensions/mv3/
- Clipboard API: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!