🧰
サイトの独自アイコン、全部"社内完結"で管理してみた ─ Nodeを使ってSVG → SCSS自動生成
📝 前書き
これまで長年 icomoon を使ってアイコンフォントを管理してきました。
便利ではあるのですが、結局は 外部ツール依存 ですし、更新のたびに手間がかかるのが正直つらい……。
さらに最近は Figma で作成したマスターデータから、
Variables 管理されたアイコンをそのまま出して使いたい、というニーズも強くなってきました。
「外部サービスに頼らず、もっと 楽に管理したい」
──そんな思いから、今回の Node.js を使った SVG → SCSS 自動生成システムを作りました。
これにより
- 外部ツール不要
- アイコンは
colorに追従して色変更可能 - 更新は SVG を置くだけ
という、数年単位で安心して使えるアイコン管理が実現しました。
🖥 前提環境
- Node.js v20以上
- パッケージ追加不要(標準モジュールのみで完結)
- プロジェクト直下で以下を実行するだけ:
node ./scripts/export-icons.mjs
📁 ディレクトリ例
project/
├─ assets/
│ └─ icons/
│ └─ svg/ ← 入力SVGを置く
├─ scripts/
│ └─ export-icons.mjs ← 変換スクリプト
└─ styles/
└─ _icons.scss ← 自動生成されるSCSS
🎯 ゴール
- ./assets/icons/svg/*.svg をスキャン
- width / height を削除、fill を除去
- ルートに fill="currentColor" を付与(JS①)
- Data URI 化して SCSS に書き出し(JS②, SCSS①)
- .i-xxx クラスでそのまま使える
- アイコン色は CSS の color に追従
export-icons.mjs
#!/usr/bin/env node
// scripts/export-icons.mjs
import fs from "node:fs/promises";
import path from "node:path";
const SRC_DIR = "./assets/icons/svg"; // 入力SVG
const OUT_FILE = "./styles/_icons.scss"; // 出力SCSS
function normalizeSvg(svg) {
return svg
// XML宣言/コメント除去
.replace(/<\?xml[\s\S]*?\?>/g, "")
.replace(/<!--[\s\S]*?-->/g, "")
// <svg> の width/height を削除
.replace(/\swidth="[^"]*"/gi, "")
.replace(/\sheight="[^"]*"/gi, "")
// すべての fill を削除
.replace(/\sfill="[^"]*"/gi, "")
// <svg> に fill="currentColor" を付与
.replace(/<svg\b/i, "<svg fill='currentColor'");
}
function toDataUri(svg) {
return `url("data:image/svg+xml;utf8,${svg
.replace(/"/g, "'")
.replace(/%/g, "%25")
.replace(/#/g, "%23")
.replace(/</g, "%3C")
.replace(/>/g, "%3E")}")`;
}
function buildScss(icons) {
const header = `// AUTO-GENERATED: Do not edit
.i {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: -.125em;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
`;
const body = icons.map(({ name, svg }) => {
const base = name.replace(/\.svg$/i, "");
return `.i-${base} { background-image: ${toDataUri(svg)}; }`;
}).join("\n");
return header + body + "\n";
}
async function main() {
const files = (await fs.readdir(SRC_DIR))
.filter(f => f.toLowerCase().endsWith(".svg"))
.sort();
const icons = [];
for (const f of files) {
const raw = await fs.readFile(path.join(SRC_DIR, f), "utf8");
icons.push({ name: f, svg: normalizeSvg(raw) });
}
await fs.mkdir(path.dirname(OUT_FILE), { recursive: true });
await fs.writeFile(OUT_FILE, buildScss(icons), "utf8");
console.log("done:", OUT_FILE);
}
main();
💡 仕上がりイメージ
入力 SVG(ビフォー):
<svg width="24" height="24" viewBox="0 0 24 24" fill="#333" xmlns="http://www.w3.org/2000/svg">
<path d="M10 18a8 8 0 1 1 5.292-14.06L22 10.65l-1.35 1.35-6.3-6.3A8 8 0 0 1 10 18z"/>
</svg>
整形後(アフター):
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M10 18a8 8 0 1 1 5.292-14.06L22 10.65l-1.35 1.35-6.3-6.3A8 8 0 0 1 10 18z"/>
</svg>
生成された SCSS(SCSS①):
.i {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: -.125em;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
.i-search {
background-image: url("data:image/svg+xml;utf8,<svg%20...%3E...</svg>");
}
🧭 実際の使い方
<span class="i i-search" aria-hidden="true"></span>
<button class="btn">
<span class="i i-mail" aria-hidden="true"></span>お問い合わせ
</button>
.btn {
font-size: 16px; // ← アイコンの基準サイズ
color: #0b6; // ← アイコン色も追従
display: inline-flex;
align-items: center;
gap: .5em;
.i { font-size: 1.25em; } // ちょい大きめ
}
✅ よくある詰まりポイント
- 色が変わらない → 生成 SVG に fill="currentColor" が入っているか確認
- 縦横比が崩れる → 入力 SVG に viewBox が必須
- にじむ → background-size: contain; を維持し、font-size で調整
- mask-imageとの違い → mask は background-color 指定が必要。currentColor 方式は color 連動で直感的
🎬 まとめ
- 新規は .i-xxx を素直に本体直書き
- 既存移行時のみ ::before 互換で対応
- 外部ツール不要、Node v20 以上で SVG → SCSS 自動生成
- これで「数年は放置できる安心アイコン管理」へ 🚀
Discussion