Claude CodeのSVGが「ダークモードで真っ黒」になる前に直す型
「SVG作って」だけだと色がハードコードされ、viewBoxが消え、ダークモードで死ぬ。僕が踏んだ罠とコピペで動く型を、アイコン・図解・最適化まで一気にまとめます。
「アイコン一式、SVGで作っといて」。
そう頼んで出てきたアイコンは、ライトモードでは完璧でした。色も形もきれい。ところがダークモードに切り替えた瞬間、検索アイコンが真っ黒な背景に真っ黒で消えたんです。fill="#172033" が全アイコンにベタ書きされていた。
しかも厄介なのはそこじゃなくて、「見た目はそれっぽく動く」から、レビューでうっかり通してしまう点でした。同じアイコンをボタン・見出し・ダークモードで使い回した瞬間に、初めて壊れる。これがSVGの罠です。
この記事の要点
- SVGの色はSVGに直接書かない。
currentColorで受けて、色はCSS側(CSS変数)に寄せる。これだけでダークモード崩壊の大半が消える。 viewBoxは座標系の土台。消すと拡大縮小できず、レスポンシブで欠ける。SVGOのremoveViewBoxは基本オフにする。- アクセシビリティは「装飾か、意味があるか」の二択。装飾は
aria-hidden="true"、意味ありはrole="img"+ ラベル。ここを機械的に間違えるとアイコンの名前が消える。 - アップロードされたSVGをそのまま画面に差し込むのは危険。SVGは
<script>を持てる。信頼できるものだけインライン化する。 - Claude Codeには「作って」ではなく「viewBox固定・currentColor・aria分岐・SVGOでviewBox残す・最後に確認コマンド」まで条件を渡す。雑な指示が雑なSVGを生む。
なぜ「SVG作って」だと毎回壊れるのか
SVGは、アイコン・ロゴ・図解・簡単なチャートをHTMLの中に直接書けるベクター画像です。拡大してもぼやけず、CSSで色を変えられ、Reactコンポーネントにも落とし込みやすい。Claude Codeとの相性はかなり良い部類です。
ただ、相性が良いことと、雑に頼んで壊れないことは別問題でした。「SVGを書いて」だけだと、僕の経験上だいたいこの4つが起きます。
viewBoxが無くて、サイズを変えると絵が欠ける- 色がハードコードされて、テーマ変更のたびにSVG本体を直すはめになる
- 装飾アイコンまでスクリーンリーダーに読まれて「検索 アイコン 検索」と二重に喋る
- どこかから拾ってきたSVGをそのまま貼って、知らないうちにスクリプトを抱える
逆に言うと、ここさえ型にしてしまえばSVGはほとんど事故りません。この記事では、初心者がClaude Codeに任せても破綻しにくい「型」を作ります。インラインSVG、viewBox、アクセシブルなアイコン、currentColor でのテーマ対応、軽いアニメーション、SVGO最適化、そしてセキュリティの落とし穴まで、コピーして動かせる例で並べます。
仕様の一次情報は、MDNのviewBox、MDNのARIA imgロール、SVGO公式ドキュメント、Claude Code公式ドキュメントあたりを見ておけば足ります。
依頼する前に決める順番
SVG操作って「絵を描く作業」だと思われがちですが、実務は違います。目的を決める → 座標を固定する → テーマ対応する → アクセシビリティを決める → 組み込む → 最適化する → レビューする、という流れがあって、絵を描くのはそのうちの一工程です。
flowchart LR
A["目的を決める"] --> B["viewBoxと座標を固定"]
B --> C["currentColorでテーマ対応"]
C --> D["aria-labelかaria-hiddenを選ぶ"]
D --> E["React/HTMLへ組み込む"]
E --> F["SVGOで最適化"]
F --> G["表示・操作・安全性をレビュー"]
Claude Codeに頼むときも、この順番をそのまま条件として渡すと品質が安定します。「SVGアイコンを作って」ではなく、「viewBox="0 0 24 24"、stroke="currentColor"、装飾アイコンは aria-hidden、意味のあるアイコンは role="img" と title、SVGOで viewBox を消さない設定まで」と書く。たったこれだけで出力の安定感がまるで違いました。
インラインSVGとviewBoxの土台
インラインSVGは、img で外部ファイルを読み込むのではなく、HTMLやJSXの中に <svg> を直接書く方法です。CSSで色や線を変えたいアイコン、状態で見た目が変わるUI部品、軽いアニメーションには、こっちが向いています。
viewBox は、SVG内部の仮想キャンバスです。MDNでは「ユーザー空間における、SVGビューポートの位置と寸法を定義する」とされていて、min-x min-y width height の4つの数値を取ります。初心者向けに言い換えると、「この絵は0から24までの方眼紙の上に描く」と宣言する設定です。
<button class="icon-button" type="button" aria-label="検索する">
<svg
class="icon"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
focusable="false"
>
<circle cx="11" cy="11" r="7" />
<path d="M20 20l-4.5-4.5" />
</svg>
</button>
この例では、ボタン側に aria-label="検索する" があるので、中のSVGは装飾扱いで aria-hidden="true" にしています。アイコン単体を読ませるのではなく、操作対象のボタンに名前を付ける。これが一番事故りにくい持ち方です。
width と height は表示サイズ、viewBox は絵の座標です。width="48" にしても viewBox="0 0 24 24" のままなら、24単位で描いた絵がそのまま48pxに拡大されます。逆に viewBox を消すと、レスポンシブで欠けたり、後述のSVGO後にスケールできなくなったりする。だから viewBox は消さない、が鉄則です。
currentColorとCSS変数でテーマ対応する
冒頭の「ダークモードで真っ黒」事件の犯人は、SVGに直接書かれた #172033 でした。SVGに #333333 や #0ea5e9 をベタ書きすると、テーマ変更・ホバー・ダークモード・ブランド色変更のたびにSVG本体を開いて直すことになります。アイコンが30個あったら、30ファイル開く羽目になる。
解決はシンプルで、色はCSS側に寄せて、SVGは currentColor を受けるだけにする。currentColor は、その要素のCSSの color 値を fill や stroke に流し込む特殊な値です。
:root {
--color-text: #172033;
--color-muted: #667085;
--color-accent: #0f766e;
--color-danger: #b42318;
}
[data-theme="dark"] {
--color-text: #eef2f7;
--color-muted: #a9b4c3;
--color-accent: #2dd4bf;
--color-danger: #f97066;
}
.icon {
color: var(--icon-color, var(--color-text));
display: inline-block;
inline-size: 1.25rem;
block-size: 1.25rem;
flex: 0 0 auto;
}
.icon-button {
color: var(--color-muted);
}
.icon-button:hover {
color: var(--color-accent);
}
.icon-button[data-variant="danger"] {
--icon-color: var(--color-danger);
}
<button class="icon-button" type="button" aria-label="削除" data-variant="danger">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
<path d="M4 7h16" />
<path d="M10 11v6" />
<path d="M14 11v6" />
<path d="M6 7l1 14h10l1-14" />
<path d="M9 7V4h6v3" />
</svg>
</button>
こうしておくと、ダークモードは [data-theme="dark"] の変数を切り替えるだけ、危険な操作のアイコンだけ赤くしたいときは data-variant="danger" を付けるだけ。SVGには一切触りません。Claude Codeにテーマ対応を頼むときは、「SVG内の fill と stroke に固定色が残っていないか検索して」と明示し、rg "fill=\"#|stroke=\"#" のような確認コマンドも一緒に走らせてもらうと、レビューが具体的になります。
Reactでアクセシブルなアイコンを作る
ここが地味に一番ミスりやすいところでした。アイコンには「装飾(意味なし)」と「意味あり」の2種類があって、扱いが正反対です。
- テキスト付きボタンの横に置く飾りのアイコン → 装飾。
aria-hidden="true"でアクセシビリティツリーから外す。 - アイコン単体で意味を持つ(アイコンだけの検索ボタンなど) → 意味あり。
role="img"と名前を付けて読ませる。
これをコンポーネント側で吸収しておくと、使う側が毎回悩まずに済みます。title を渡したときだけ読み上げ対象になり、decorative を立てたら隠れる、という設計です。
import { useId } from "react";
type IconName = "search" | "check" | "close";
const paths: Record<IconName, string> = {
search: "M10.5 18a7.5 7.5 0 1 1 5.3-12.8 7.5 7.5 0 0 1-5.3 12.8Zm5.3-2.2L21 21",
check: "M5 12.5l4.5 4.5L19 7",
close: "M6 6l12 12M18 6L6 18",
};
type SvgIconProps = {
name: IconName;
title?: string;
decorative?: boolean;
size?: number;
className?: string;
};
export function SvgIcon({
name,
title,
decorative = false,
size = 24,
className,
}: SvgIconProps) {
const titleId = useId();
// 装飾でなく、かつtitleがあるときだけ「意味あり」として扱う
const isMeaningful = !decorative && Boolean(title);
return (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
role={isMeaningful ? "img" : undefined}
aria-labelledby={isMeaningful ? titleId : undefined}
aria-hidden={decorative ? true : undefined}
focusable="false"
>
{isMeaningful ? <title id={titleId}>{title}</title> : null}
<path d={paths[name]} />
</svg>
);
}
使い分けはこうです。
<button type="button">
<SvgIcon name="check" decorative />
保存する
</button>
<SvgIcon name="search" title="検索" />
1つ目はボタンのテキスト「保存する」が操作名なので、アイコンは装飾。2つ目はアイコン自体が意味を持つので title を渡します。注意点として、aria-hidden="true" をフォーカスできる要素や重要なリンクに付けるのは避けてください。MDNも、フォーカス可能な要素に aria-hidden="true" を使わないよう警告しています。僕はここを機械的に全アイコンへ付けて、アイコンだけの検索ボタンの名前を丸ごと消したことがあります。
軽いアニメーションを「邪魔せず」入れる
SVGアニメーションは、ローディング、成功チェック、グラフの強調などに使えます。ただ記事や管理画面では、派手さより「本文の邪魔をしない」「prefers-reduced-motion を尊重する」「レイアウトを揺らさない」が優先です。動きに弱い人もいるので、止められない動きは普通に害になります。
次はCSSだけで動くローディングアイコンです。SVGのサイズは固定して、CSS側で回転だけを制御します。
<svg class="spinner" viewBox="0 0 48 48" width="48" height="48" role="img" aria-label="読み込み中">
<circle class="spinner-track" cx="24" cy="24" r="20" />
<circle class="spinner-head" cx="24" cy="24" r="20" />
</svg>
.spinner {
color: #0f766e;
animation: spin 900ms linear infinite;
}
.spinner-track,
.spinner-head {
fill: none;
stroke-width: 4;
}
.spinner-track {
stroke: #d0d5dd;
}
.spinner-head {
stroke: currentColor;
stroke-linecap: round;
stroke-dasharray: 80 45;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 動きを減らしたい設定の人には回転を止める */
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: none;
}
}
prefers-reduced-motion の1ブロックを入れるかどうかだけで、配慮の有無がはっきり分かれます。CSS側の表現をもっと深掘りするなら、Claude Code CSSアニメーション高度テクニックにもつながります。SVGはベクターの「形」、CSSは「状態と動き」、と役割を分けると保守が楽になります。
データをSVGで描く最小例
SVGは簡単なチャートにも使えます。外部ライブラリを入れるほどでもない小さな棒グラフなら、TypeScriptでSVG文字列を生成してしまうのが手軽です。ここで一つだけ落とし穴があって、ユーザー入力を <text> に入れるならXMLエスケープが必須です。エスケープを忘れると表示が崩れるどころか、入力経由で要素を注入される穴になります。
type BarDatum = {
label: string;
value: number;
};
// <, >, &, ", ' をエンティティに変換してSVGへの注入を防ぐ
function escapeXml(value: string): string {
return value.replace(/[<>&"']/g, (char) => {
const entities: Record<string, string> = {
"<": "<",
">": ">",
"&": "&",
'"': """,
"'": "'",
};
return entities[char];
});
}
export function createMiniBarChart(data: BarDatum[]): string {
const width = 420;
const height = 180;
const padding = 32;
const gap = 12;
const maxValue = Math.max(...data.map((item) => item.value), 1);
const barWidth = (width - padding * 2 - gap * (data.length - 1)) / data.length;
const bars = data
.map((item, index) => {
const barHeight = (item.value / maxValue) * 100;
const x = padding + index * (barWidth + gap);
const y = height - padding - barHeight;
return `
<rect x="${x}" y="${y}" width="${barWidth}" height="${barHeight}" rx="6" fill="currentColor" />
<text x="${x + barWidth / 2}" y="${height - 10}" text-anchor="middle" font-size="12">
${escapeXml(item.label)}
</text>`;
})
.join("");
return `<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="月別の問い合わせ数" xmlns="http://www.w3.org/2000/svg">
<g color="#0f766e">${bars}</g>
</svg>`;
}
これくらいの規模なら、社内ダッシュボード、記事内の小さな比較図、LPの実績表示には十分です。軸・凡例・ツールチップ・大量データが要るなら、素直に専用チャートライブラリに切り替える判断もしてください。SVGで全部やろうとして沼る、というのも僕がやった失敗の一つです。
SVGOで最適化する(viewBoxは死守)
FigmaやIllustratorから書き出したSVGには、エディタ由来の属性、不要なメタデータ、桁の多い小数が残りがちです。SVGOはそれを落とすSVG最適化ツールで、公式ドキュメントによるとCLI・Node.jsライブラリ・ブラウザ・Webpack loaderの4経路で使えます(現行はv4系)。
最初は viewBox を守る設定にして、差分を目で確認しながら使うのが安全です。removeDimensions は最上位 svg の width と height を削って、必要なら viewBox に寄せるプラグイン。一方 removeViewBox はスケールできなくなる危険があるので、レスポンシブなアイコンでは基本的に避けます。SVGO v4のpreset-defaultでは removeViewBox は既定で無効ですが、念のため明示しておくと安心です。
// svgo.config.mjs
export default {
multipass: true,
plugins: [
{
name: "preset-default",
params: {
overrides: {
cleanupIds: false,
// viewBoxは絶対に残す(明示しておく)
removeViewBox: false,
},
},
},
"removeDimensions",
{
name: "removeAttrs",
params: {
attrs: ["data-name"],
},
},
],
};
{
"scripts": {
"svg:optimize": "svgo --config svgo.config.mjs --folder src/assets/icons"
},
"devDependencies": {
"svgo": "^4.0.0"
}
}
Claude Codeには、設定を作らせるだけでなく、レビューまで込みで頼みます。雑に「最適化して」と言うと、平気で viewBox を消してくることがあるからです。
src/assets/icons配下のSVGをSVGOで最適化したいです。
viewBoxは絶対に残してください。
currentColorでテーマ変更できるか確認してください。
before/afterのファイルサイズ、消えた属性、見た目に影響しそうな差分を表で報告してください。
こんな場面で効く(4つ)
1つ目は、記事サイトやSaaS管理画面のアイコンシステム。検索・閉じる・保存・警告・外部リンクのような小さなアイコンを、viewBox="0 0 24 24"・currentColor・Reactコンポーネントで統一すると、色とサイズをUI側から一括で制御できます。
2つ目は、技術記事の概念図。コードだけだと読者が離脱する場面で、箱と矢印とラベルのSVG図解を挟むと理解が進みます。Claude Codeに「この説明をSVG図解にして」と頼むときは、テキスト量を絞り、スマホ幅でも読めるサイズにする条件を必ず付けます。
3つ目は、LPや教材販売ページの収益導線。料金表・チェックリスト・購入ボタンの近くに軽いSVG図解を置くと、ただの装飾ではなく「購入前の不安を減らす情報」になります。CTAを邪魔しない図解、がコツです。
4つ目は、管理画面の小さなデータ表示。問い合わせ件数、読了率、CTAクリック率くらいの小さな比較なら、前述のミニ棒グラフSVGで足ります。いきなり複雑なグラフを作る前に、最小のSVGで読めるか試すと実装が軽く済みます。
| 使い方 | 向いているSVG形式 | 注意点 |
|---|---|---|
| アイコン | インラインSVG、Reactコンポーネント | 装飾か意味ありかを分ける |
| ロゴ | imgまたはインラインSVG | ブランド色と代替テキストを確認する |
| 図解 | インラインSVG | スマホで文字が潰れないようにする |
| 小さなチャート | 生成SVG | ユーザー入力をエスケープする |
失敗例とセキュリティの落とし穴
SVGはHTMLに近い表現力を持ちます。MDNにあるとおりSVGは <script> 要素を持てて、SVG文書にスクリプトを仕込めます。つまり、ユーザーがアップロードしたSVGを無条件でインライン挿入するのは、XSSを自分で招き入れるようなものです。信頼できるSVGだけをインライン化し、それ以外は img で読むか、サニタイズを通します。
僕が踏んだ/踏みかけた罠を表にまとめます。だいたいこのどれかで一度はやられます。
| 失敗例 | 何が起きるか | 対策 |
|---|---|---|
viewBoxを消す | レスポンシブで欠ける、拡大縮小できない | SVGO設定とレビューで残す |
fill="#000"が残る | ダークモードやホバーで色が変わらない | currentColorへ寄せる |
意味のあるアイコンにaria-hidden | 支援技術へ操作の意味が伝わらない | ボタンかSVGへ適切な名前を付ける |
| 装飾アイコンを読み上げる | 「検索 アイコン 検索」と重複する | 装飾はaria-hidden="true" |
| アップロードSVGを直接挿入 | スクリプトやイベント属性のリスク | 信頼できるSVGだけインライン化する |
| アニメーションを止められない | 動きに弱い読者へ負担が出る | prefers-reduced-motionを使う |
idが衝突する | グラデーションやマスクが別アイコンへ影響する | useIdやSVGO設定を確認する |
Claude Code側の安全運用もセットです。Claude Code公式ドキュメントでは権限管理や操作時の許可について説明されています。SVG最適化のようにファイルを一括変更する作業では、最初に対象ディレクトリを限定し、git diff --check と差分レビューを必ず挟む。これを習慣にしてから、一括変換でやらかすことが激減しました。
そのままClaude Codeに渡せる依頼文
仕上げに、コピペで使える依頼文を置いておきます。アイコンを作るだけでなく、レビュー観点まで含めているのがポイントです。
このリポジトリの既存デザインに合わせて、SVGアイコンコンポーネントを追加してください。
条件:
- viewBoxは0 0 24 24で統一
- fillまたはstrokeはcurrentColorを使う
- テキスト付きボタン内の装飾アイコンはaria-hidden=true
- アイコン単体で意味がある場合はrole=imgとtitleを使う
- prefers-reduced-motionを尊重した簡単なローディングSVGも追加
- SVGO設定を作る。ただしviewBoxは削除しない
- 最後に失敗例、セキュリティ注意点、確認コマンドを報告
実装後に見るチェックリストは、これくらい短くて十分です。
viewBoxが残っている- 固定色が必要なロゴ以外は
currentColorを使っている - ボタン・リンク・アイコン単体のアクセシブル名が重複していない
- 375px幅で図解の文字が読める
- SVGO後も見た目が変わっていない
- ユーザー投稿SVGをインライン挿入していない
- 変更範囲がアイコン・CSS・設定に限定されている
表示速度まで気にするならClaude Codeパフォーマンス最適化も合わせてどうぞ。SVGは軽い部品ですが、数が増えると塵も積もります。
よくある質問
Q. SVGの色は fill と currentColor、どっちで指定すべき?
A. アイコンやUI部品は currentColor です。色をCSSの color に寄せておけば、ダークモード・ホバー・ブランド色変更をCSSだけで吸収できます。固定色が決まっているロゴだけは例外で、ブランドカラーをそのまま持たせて構いません。
Q. viewBox は消してもいい?
A. レスポンシブに使うなら消さないでください。viewBox が無いとSVGがスケールできず、サイズを変えると絵が欠けます。SVGOでも removeViewBox は無効のままにするのが安全です。
Q. アイコンに aria-label を付けるべき? aria-hidden にすべき?
A. 周りにテキストがあるか次第です。テキスト付きボタンの中の飾りなら aria-hidden="true"。アイコンだけで意味を持つ(アイコンのみの検索ボタンなど)なら role="img" と名前を付けます。両方読ませると「検索 アイコン 検索」と重複するので、どちらか一方にします。
Q. ダウンロードしたSVGをそのままサイトに貼っても大丈夫?
A. 出どころが信頼できるものだけにしてください。SVGは <script> やイベント属性を持てるので、ユーザー投稿やフリー素材を無検査でインライン挿入するとXSSの穴になります。不安なら img で読み込むか、サニタイズを通します。
Q. Claude CodeにSVGを頼むとき、何を書けば失敗が減る? A. 「viewBox固定・currentColor・装飾はaria-hidden・意味ありはrole=img・SVGOでviewBox残す・最後に確認コマンドを報告」までを条件として渡してください。「アイコン作って」だけだと、色のハードコードとviewBox削除でほぼ確実に一度はやられます。
実際に試した結果
冒頭の「ダークモードで真っ黒」事件のあと、僕はSVGのレビュー観点を「見た目」から「型に従っているか」に切り替えました。検証用UIで固定色を currentColor に寄せ、viewBox を残すSVGO設定にしただけで、ライト・ダーク・警告ボタンの3パターンを同じアイコン1個で回せるようになりました。
逆に、最初に焦って全アイコンへ機械的に aria-hidden を付けたときは、アイコンだけの検索ボタンの名前がごっそり消えました。SVGは小さな部品ですが、Claude Codeに「見た目・アクセシビリティ・最適化・安全性」を同時にレビューさせると、公開前の手戻りが目に見えて減ります。賢く描かせるより、壊れない型を先に渡す。SVGに関しては、これが一番速い遠回りでした。
自分のプロジェクトでSVG・アイコン設計・CLAUDE.md・レビュー観点まで一式整えたいなら、ClaudeCodeLabの教材・テンプレート一覧で土台を作るか、チーム導入ならClaude Code導入相談・研修で実リポジトリ前提の設計を相談してください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。