注意: この記事は2026年3月時点の情報に基づいています。AI関連のツールや機能は頻繁に更新されるため、情報が古くなっている可能性があります。また、ここで紹介する方法は特定の環境での検証に基づいており、すべてのケースで正しく動作する保証はありません。ご自身の環境に合った形に適宜変換してお読みください。
はじめに
現在開発中のサービスのUIをFigmaで整理する取り組みを行っています。これまではデザイナーがFigmaで作ったデザインを実装するという一方通行のフローでしたが、今回は「本番環境のUIをFigmaに取り込んで整理する」という逆向きのアプローチを試みています。
この取り組みには、Claude CodeとFigma MCP(Model Context Protocol)を活用しています。Figma MCPにはgenerate_figma_designという機能があり、HTMLを渡すとFigmaデザインを自動生成してくれます。
しかし、いざ本番環境のURLを指定してキャプチャしてみると、いくつかの落とし穴に直面しました。この記事では、それらの落とし穴と、Playwrightを使った回避策についてまとめます。
Figma MCPの基本的な使い方
Figma MCPのgenerate_figma_designは、以下のような流れで使います:
- Playwrightで対象URLを開く
-
mcp__figma__generate_figma_designを実行 - Figmaにデザインが生成される
本来はこれだけで十分なはずでした。
落とし穴その1:擬似要素(::before, ::after)がキャプチャされない
最初に直面した問題は、CSS擬似要素が正しくキャプチャされないことでした。
.required-label::before {
content: "※";
color: red;
}
このような擬似要素は、Figma captureでは無視されてしまいます。必須項目を示す「※」や、アイコンなどを擬似要素で実装している部分が消滅しました。
落とし穴その2:CSSカウンターが評価されない
自動採番などのために使われるCSSカウンターも、Figma captureでは正しく処理されません。
.steps-list {
counter-reset: step;
}
.step-item::before {
content: counter(step);
counter-increment: step;
}
擬似要素内のcounter()関数は評価されず、そのまま表示されてしまいます。
落とし穴その3:background-imageがキャプチャされない
CSSのbackground-imageプロパティで指定された画像は、Figma captureでは正しく処理されません。
.hero-section {
background-image: url("/images/hero.jpg");
background-size: cover;
}
<img>タグは処理できますが、background-imageは無視されてしまいます。画像が空白になる問題が発生しました。
落とし穴その4:pictureタグ
レスポンシブ画像のために使われる<picture>タグが正しく処理されませんでした。
<picture>
<source srcset="/images/avatar.avif" type="image/avif" />
<img src="/images/avatar.jpg" />
</picture>
Figma captureは<picture>を正しく解釈できず、画像が正しく表示されません。
落とし穴その5:Figmaが対応していない画像形式
本番環境では、AVIFという軽量な画像形式が使われています。しかし、Figmaはこれらの形式に対応していないため、画像が空白になる問題が発生しました。
回避策:PlaywrightでDOMを前処理する
これらの問題に対処するため、Playwrightを使ってFigma captureの前にDOMを前処理することにしました。
基本的なアプローチは以下の通りです:
- Playwrightで対象URLを開く
- JavaScriptを注入してDOMを変換する
- Figma captureを実行する
DOM前処理スクリプトの注入
次に、DOMを前処理するスクリプトを注入します。このスクリプトで以下の処理を行います:
- 擬似要素を実DOM要素に変換(::before, ::after)
- CSSカウンターを実際の数字に変換
// CSSカウンターを実際の数字に変換
function fixCssCounters() {
const all = document.querySelectorAll("*");
const counters = {};
all.forEach(el => {
const styles = window.getComputedStyle(el, "::before");
const beforeContent = styles.getPropertyValue("content");
if (beforeContent && beforeContent.includes("counter(")) {
const counterMatch = beforeContent.match(/counter\\(([^)]+)\\)/);
if (counterMatch) {
counters[counterMatch[1]] = (counters[counterMatch[1]] || 0) + 1;
const value = counters[counterMatch[1]];
const beforeEl = document.createElement("span");
beforeEl.className = "figma-counter-before";
beforeEl.textContent = value.toString();
// スタイルをコピー
const properties = [
"display", "position", "float", "clear", "top", "right", "bottom", "left",
"width", "height", "margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
"padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
"border", "border-top", "border-right", "border-bottom", "border-left",
"color", "background", "background-color", "background-image",
"font", "font-size", "font-weight", "font-family", "font-style",
"text-align", "vertical-align", "line-height", "text-decoration",
"z-index", "opacity", "visibility"
];
properties.forEach(prop => {
const val = styles.getPropertyValue(prop);
const priority = styles.getPropertyPriority(prop);
if (val && val !== "none" && val !== "normal" && val !== "auto" && val !== "") {
beforeEl.style.setProperty(prop, val, priority);
}
});
el.insertBefore(beforeEl, el.firstChild);
}
}
});
return Object.keys(counters).length;
}
// 擬似要素を変換(カウンター以外)
function convertPseudoElements() {
const all = document.querySelectorAll("*");
let convertedCount = 0;
all.forEach(el => {
// ::beforeを処理(カウンター以外)
const beforeStyles = window.getComputedStyle(el, "::before");
const beforeContent = beforeStyles.getPropertyValue("content");
if (beforeContent && beforeContent !== "none" && beforeContent !== "normal" && beforeContent !== "" && !beforeContent.includes("counter(")) {
const beforeEl = document.createElement("span");
beforeEl.className = "figma-pseudo-before";
beforeEl.setAttribute("data-pseudo", "before");
beforeEl.textContent = beforeContent.replace(/^["']|["']$/g, "");
const properties = [
"display", "position", "float", "clear", "top", "right", "bottom", "left",
"width", "height", "min-width", "max-width", "min-height", "max-height",
"margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
"padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
"border", "border-top", "border-right", "border-bottom", "border-left",
"border-width", "border-style", "border-color",
"border-radius", "border-top-left-radius", "border-top-right-radius",
"border-bottom-left-radius", "border-bottom-right-radius",
"color", "background", "background-color", "background-image",
"font", "font-size", "font-weight", "font-family", "font-style",
"text-align", "vertical-align", "line-height", "text-decoration",
"z-index", "opacity", "visibility"
];
properties.forEach(prop => {
const value = beforeStyles.getPropertyValue(prop);
const priority = beforeStyles.getPropertyPriority(prop);
if (value && value !== "none" && value !== "normal" && value !== "auto" && value !== "") {
beforeEl.style.setProperty(prop, value, priority);
}
});
el.insertBefore(beforeEl, el.firstChild);
convertedCount++;
}
// ::afterも同様に処理
const afterStyles = window.getComputedStyle(el, "::after");
const afterContent = afterStyles.getPropertyValue("content");
if (afterContent && afterContent !== "none" && afterContent !== "normal" && afterContent !== "" && !afterContent.includes("counter(")) {
const afterEl = document.createElement("span");
afterEl.className = "figma-pseudo-after";
afterEl.setAttribute("data-pseudo", "after");
afterEl.textContent = afterContent.replace(/^["']|["']$/g, "");
const properties = [
"display", "position", "float", "clear", "top", "right", "bottom", "left",
"width", "height", "min-width", "max-width", "min-height", "max-height",
"margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
"padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
"border", "border-top", "border-right", "border-bottom", "border-left",
"border-width", "border-style", "border-color",
"border-radius", "border-top-left-radius", "border-top-right-radius",
"border-bottom-left-radius", "border-bottom-right-radius",
"color", "background", "background-color", "background-image",
"font", "font-size", "font-weight", "font-family", "font-style",
"text-align", "vertical-align", "line-height", "text-decoration",
"z-index", "opacity", "visibility"
];
properties.forEach(prop => {
const value = afterStyles.getPropertyValue(prop);
const priority = afterStyles.getPropertyPriority(prop);
if (value && value !== "none" && value !== "normal" && value !== "auto" && value !== "") {
afterEl.style.setProperty(prop, value, priority);
}
});
el.appendChild(afterEl);
convertedCount++;
}
});
console.log("擬似要素変換完了: " + convertedCount + "個");
return convertedCount;
}
// 実行
const counterResult = fixCssCounters();
const pseudoResult = convertPseudoElements();
return { counters: counterResult, pseudos: pseudoResult };
background-imageのimgタグへの変換
background-imageは<img>タグに変換する必要があります。これは前処理スクリプトに以下の処理を追加することで対応できます。
// background-imageを使っている要素をimgタグに変換
function convertBackgroundImages() {
const all = document.querySelectorAll("*");
let convertedCount = 0;
all.forEach(el => {
const styles = window.getComputedStyle(el);
const bgImage = styles.getPropertyValue("background-image");
// url()を含むbackground-imageを処理
if (bgImage && bgImage !== "none" && bgImage.includes("url(")) {
const urlMatch = bgImage.match(/url\\(['"]?([^'")]+)['"]?\\)/);
if (urlMatch) {
const imageUrl = urlMatch[1];
// img要素を作成
const img = document.createElement("img");
img.src = imageUrl;
img.className = el.className;
img.setAttribute("data-converted-from-bg", "true");
// 背景のサイズをimgのスタイルに反映
const bgSize = styles.getPropertyValue("background-size");
const bgPosition = styles.getPropertyValue("background-position");
const bgRepeat = styles.getPropertyValue("background-repeat");
if (bgSize && bgSize !== "auto") {
img.style.width = bgSize === "cover" || bgSize === "contain" ? "100%" : bgSize;
img.style.height = bgSize === "cover" || bgSize === "contain" ? "100%" : bgSize;
img.style.objectFit = bgSize;
}
if (bgPosition) {
img.style.objectPosition = bgPosition;
}
// 元の要素の背景をクリアしてimgを挿入
el.style.backgroundImage = "none";
el.insertBefore(img, el.firstChild);
convertedCount++;
}
}
});
console.log("background-image変換完了: " + convertedCount + "個");
return convertedCount;
}
Figma Design Preflightスキルの活用
pictureタグの平坦化やURLの絶対URL化などは、figma-design-preflightスキルで対応できます。
const preflightScript = await fs.promises.readFile('.claude/skills/figma-design-preflight/scripts/browser-preflight.js', 'utf-8');
await page.evaluate(preflightScript);
await page.evaluate(() => {
return window.figmaPreflight.run({
flattenPicture: true, // picture -> img 変換
});
});
このスキルは以下の処理を自動で行います:
-
src/data-src/posterの絶対URL化 -
srcset/data-srcsetの絶対URL化 -
style属性・<style>内url(...)の絶対URL化 -
picture→imgへのフラット化
完全な実行フロー
最終的な実行フローは以下のようになります:
async (page) => {
// ビューポートを375pxに設定
await page.setViewportSize({ width: 375, height: 812 });
// ページの前処理スクリプト(擬似要素・CSSカウンターの変換)
const preProcessScript = `
(function() {
// CSSカウンターを実際の数字に変換
function fixCssCounters() { ... }
// 擬似要素を変換(カウンター以外)
function convertPseudoElements() { ... }
// background-imageをimgタグに変換
function convertBackgroundImages() { ... }
fixCssCounters();
convertPseudoElements();
convertBackgroundImages();
})();
`;
await page.evaluate(preProcessScript);
// figma-design-preflightスキルを実行(pictureタグ対策)
const preflightScript = await fs.promises.readFile('.claude/skills/figma-design-preflight/scripts/browser-preflight.js', 'utf-8');
await page.evaluate(preflightScript);
await page.evaluate(() => {
return window.figmaPreflight.run({
flattenPicture: true,
});
});
// capture.jsを注入
const response = await page.context().request.get("https://mcp.figma.com/mcp/html-to-design/capture.js");
const script = await response.text();
await page.evaluate((s) => {
const el = document.createElement("script");
el.textContent = s;
document.head.appendChild(el);
}, script);
// キャプチャ実行
await page.waitForTimeout(500);
return await page.evaluate(() => window.figma.captureForDesign({
captureId: "your-capture-id",
endpoint: "https://mcp.figma.com/mcp/capture/your-capture-id/submit",
selector: "body"
}));
}
まとめ
| 問題 | 回避策 |
|---|---|
| 擬似要素がキャプチャされない | getComputedStyleで実DOM要素に変換 |
| CSSカウンターが評価されない | カウンター値を計算して実DOMに変換 |
| background-imageがキャプチャされない | getComputedStyleでURLを抽出し<img>タグに変換 |
| pictureタグが正しく処理されない |
figma-design-preflightで<img>に平坦化 |
| AVIF画像が表示されない | PNG形式に変換が必要(別途対応) |
Figma MCPは非常に便利ですが、本番環境の複雑なHTMLを扱う場合は、いくつかの落とし穴があります。
Playwrightを使ってDOMを前処理することで、これらの問題の多くを回避できます。特に擬似要素の変換、CSSカウンターの対応、background-imageの<img>タグへの変換は、UIを正しくFigmaに取り込むために重要な対応です。
なお、AVIFなどの画像形式については、PNG形式への変換など、別途対応が必要です。また、pictureタグについては、figma-design-preflightスキルで対応しています。
このアプローチを使うことで、本番環境のUIをFigmaに取り込み、デザイン整理や、デザイナーとの共有をスムーズに行えるようになりました。
同じように「本番環境のUIをFigmaに整理したい」と考えている方の参考になれば幸いです。