ExcelJS の使い方: ブラウザで .xlsx を生成・ダウンロードする
ExcelJS とは何かから、装飾(太字・背景色・罫線・セル結合)、数式、数値書式、ダウンロードまでを動く demo と最小コードで。10 万行での SheetJS とのサイズ・速度の実測と、はまりやすい 7 つの落とし穴つき。
検証日: 2026-09-14(初版 2026-05-10)
使用バージョン:
exceljs@4.4.0(2026-05 時点でlatest)対象: ブラウザでユーザに
.xlsxをダウンロードさせたい / Node 上で帳票生成したい場面
ExcelJS で .xlsx をブラウザ完結で生成・ダウンロード するパターン。書式付きセル / 数式 / 複数シート / 画像埋め込み / Node 出力までを動く demo で確認します。
触って試す
ボタンを押すと、装飾ヘッダ + 数式 + 通貨書式付きの xlsx が生成されてダウンロードされる。
この実装を体系立てて学ぶ本(ブラウザでファイルを作る処理は、Blob / File API と非同期処理の理解で安定する)
改訂3版JavaScript本格入門
ES2022対応の文法基礎からオブジェクト指向構文まで一冊で体系的に押さえる
実践Node.js入門ー基礎・開発・運用
Node.jsの基礎文法から非同期処理・CLI・Express実装まで一気通貫で学ぶ。フロントエンド開発の経験があり、Node.jsでのサーバーサイド開発を体系的に学び直したい人向け。
ExcelJS とは、xlsx 生成とは(30 秒版)
ExcelJS は、JavaScript だけで Excel ファイル(.xlsx)を 作る・読む・書き換える ためのライブラリ。Node.js でもブラウザでも同じ API で動き、セルの値だけでなく、太字・背景色・罫線・数値書式・数式・結合セル・列幅といった Excel 上の見た目と振る舞い まで指定できる。
xlsx を生成する とは、内部的には Office Open XML という規格の XML ファイル群を組み立てて zip にまとめること。ExcelJS はその組み立てを全部引き受けるので、書く側は「シートに行を足す」「このセルを太字にする」というオブジェクト操作だけで済む。Excel 本体も、サーバ側の Office も要らない。
読み込み専用でよい、または速度と軽さを優先するなら SheetJS(xlsx)も選択肢になる。違いは末尾の「100,000 行で実測」と「向く / 向かないケース」にまとめた。
セットアップ(npm 1 行 / Node + Browser 同 API)
npm i exceljs
| 項目 | 値 |
|---|---|
| 公開バージョン | 4.4.0 |
| Node 必要バージョン | 18+ |
| TypeScript 型 | 本体同梱(別途 @types/exceljs は不要、deprecated) |
| ブラウザ対応 | UMD bundle 同梱(exceljs/dist/exceljs.min.js)、ESM でも import 可 |
| バンドル影響 | min+gzip で 200KB 前後(フォントサブセット内部リソースあり)、main bundle に同梱せず動的 import 推奨 |
| ライセンス | MIT |
ブラウザで使うなら import ExcelJS from "exceljs"。Vite / webpack で警告が出る場合は optimizeDeps.include に exceljs を入れる。Cloudflare Workers のような Web API しかない実行環境 では fs 依存の writeFile は使えないので、writeBuffer() + Response で配信する形になる(後述)。
最小サンプル(ExcelJS 4.4、ブラウザで 5 ステップ)
ワークブックを 1 つ作って、ヘッダ + 行を流し込み、Blob 化して .xlsx をダウンロードするまでの最小コード:
import ExcelJS from "exceljs";
const wb = new ExcelJS.Workbook();
const sheet = wb.addWorksheet("Report");
sheet.columns = [
{ header: "ID", key: "id", width: 8 },
{ header: "Name", key: "name", width: 24 },
{ header: "Total", key: "total", width: 12 },
];
sheet.addRow({ id: 1, name: "Alice", total: 1500 });
sheet.addRow({ id: 2, name: "Bob", total: 1100 });
const buf = await wb.xlsx.writeBuffer();
const blob = new Blob([buf], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "report.xlsx";
a.click();
URL.revokeObjectURL(url);
writeBuffer() で ArrayBuffer を取得 → Blob でダウンロード。
装飾 2 種(ヘッダ太字 + 背景色 ARGB)
ヘッダ行の font を太字に、塗りつぶしを pattern: "solid" で指定:
sheet.getRow(1).font = { bold: true };
sheet.getRow(1).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFE2E8F0" }, // 8 桁 ARGB(先頭は alpha = FF)
};
ARGB の先頭 2 桁は alpha(FF = 不透明)。HTML の hex とは桁数が違うので注意。
数式 + 集計(SUM の 2 パターン)
セル値の代わりに { formula: "..." } を渡すと、Excel で開いた時に計算式として扱われる:
sheet.addRow({
id: 1, qty: 5, price: 200,
total: { formula: "C2*D2" }, // セル参照
});
sheet.addRow({
id: "", qty: { formula: "SUM(C2:C10)" }, price: "",
total: { formula: "SUM(E2:E10)" },
});
{ formula: "..." } で数式を埋める。Excel が開いた時に値が自動計算される。
数値書式 3 種(通貨 / パーセント / 日付)
列単位で numFmt(Excel の番号書式コード)を当てると、各セルの表示が一括で揃う:
sheet.getColumn("price").numFmt = '"¥"#,##0';
sheet.getColumn("rate").numFmt = "0.00%";
sheet.getColumn("date").numFmt = "yyyy/mm/dd";
Excel の番号書式コードがそのまま使える。
罫線 4 辺 + mergeCells
全セルに罫線を引いて、ヘッダ行は mergeCells で 1 行ぶん結合:
// すべてのセルに罫線
sheet.eachRow((row) => {
row.eachCell((cell) => {
cell.border = {
top: { style: "thin" }, bottom: { style: "thin" },
left: { style: "thin" }, right: { style: "thin" },
};
});
});
// セルマージ
sheet.mergeCells("A1:C1");
装飾した Excel をダウンロードする(完成例)
ここまでの装飾(ヘッダの太字と背景色、罫線、数値書式、折り返し、列幅)を 1 つにまとめ、ブラウザでダウンロードさせるまでの完成形。writeBuffer() の戻り値は Node なら Buffer、ブラウザなら ArrayBuffer だが、Blob はどちらも受け取れる。
import ExcelJS from "exceljs";
export async function downloadStyledXlsx(rows: { item: string; qty: number; date: Date }[]) {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet("data");
ws.columns = [
{ header: "品目", key: "item", width: 28 },
{ header: "数量", key: "qty", width: 10 },
{ header: "日付", key: "date", width: 12 },
];
ws.addRows(rows);
const header = ws.getRow(1);
header.font = { bold: true };
header.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFE2E8F0" } };
header.alignment = { vertical: "middle", horizontal: "center" };
const thin = { style: "thin" as const };
ws.eachRow((row, n) => {
row.eachCell((cell, c) => {
cell.border = { top: thin, left: thin, bottom: thin, right: thin };
if (n === 1) return;
if (c === 1) cell.alignment = { wrapText: true }; // 長い品目名は折り返す
if (c === 2) cell.numFmt = "#,##0"; // 3 桁区切り
if (c === 3) cell.numFmt = "yyyy-mm-dd"; // Date をそのまま渡し、表示だけ書式で決める
});
});
const buf = await wb.xlsx.writeBuffer();
const blob = new Blob([buf], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement("a"), { href: url, download: "report.xlsx" });
a.click();
URL.revokeObjectURL(url);
}
装飾はどれくらい重いのか。1,000 行 × 10 列を「装飾なし」「ヘッダのみ」「全セルに罫線 + 書式 + 折り返し」の 3 通りで書き出し、生成時間とサイズを測った(Node 22.17、exceljs@4.4.0、3 回の中央値、2026-09-14。計測スクリプト)。
テーブルで選択した行だけを出力したい場合は、TanStack Table v9 の「選択した行だけを xlsx に出す」 に、選択状態のどこから行を取るかで出力行数が 120 行にも 1,000 行にもなる実測があります。
CSV ↔ xlsx の 2 方向変換
wb.csv / wb.xlsx でフォーマット間を相互変換できる(Node 想定、stream API):
// CSV を xlsx に
const wb = new ExcelJS.Workbook();
await wb.csv.read(stream); // Node の Readable Stream
await wb.xlsx.writeFile("out.xlsx");
// xlsx を CSV に
await wb.xlsx.readFile("in.xlsx");
await wb.csv.writeFile("out.csv");
Cloudflare Workers / Edge で 1 endpoint .xlsx 配信
exceljs の writeBuffer() は Buffer | ArrayBuffer を返すので、Node fs を経由せず Response にそのまま流せる:
export async function onRequestGet() {
const ExcelJS = await import("exceljs");
const wb = new ExcelJS.Workbook();
const sheet = wb.addWorksheet("Report");
sheet.addRow(["id", "name"]);
sheet.addRow([1, "Alice"]);
// Node では Buffer、ブラウザ / Workers では ArrayBuffer
const buf = await wb.xlsx.writeBuffer();
return new Response(buf, {
headers: {
"Content-Type":
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Content-Disposition": 'attachment; filename="report.xlsx"',
},
});
}
Workers のサイズ制限(1MB scripts、25MB free / 100MB paid)を超えないように、exceljs は dynamic import で route 単位にロードするのが安全。fonts / 画像など重い機能を使わなければ実用範囲に収まる。
7 つの落とし穴と直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
| 塗りが反映されない | ARGB の 先頭 2 桁が alpha | HTML hex E2E8F0 の前に FF を付けて FFE2E8F0 にする |
| 配列 index で行を指定すると ずれる | Cell reference は 1-based(C2 = 2 行目) | sheet.getRow(1) がヘッダ、getRow(2) が 1 行目データ |
writeBuffer() の戻り値が型エラー | 環境で Buffer (Node) / ArrayBuffer (Browser) が違う | Blob([buf]) は両方受け取るので Blob 化してから扱う |
| 数式の参照がズレる | addRow で動的に行が増えると数式の絶対セル参照も追従しない | formula を文字列補完で組み立てる(sheet.rowCount で最終行を取って SUM(C2:C + n + ) のように生成) |
| mergeCells 後に値を入れたのに表示されない | マージ後は 左上セル以外の値が無視される | mergeCells する前に値を入れるか、左上セル getCell("A1") に書く |
通貨が ¥1,500.00 になってしまう | numFmt のコード末尾に小数指定 | '"¥"#,##0'(整数)/ '"¥"#,##0.00'(小数 2 桁)を使い分け |
| 10 万行で OOM | workbook を全部 memory に保持 | wb.xlsx.write(stream) で書き出し(Node)、または xlsx-populate / node-xlsx 等の軽量代替 |
100,000 行で実測: サイズと速度(ExcelJS vs SheetJS)
5 列 × 100,000 行を同じデータで書き出し、読み戻した(Node 22.17、exceljs@4.4.0 / xlsx@0.20.3、3 回の中央値、2026-09-05)。
評価
| 観点 | 評価 | コメント |
|---|---|---|
| 学習コスト | ○ | API は分かりやすいが、書式コード仕様(numFmt)に Excel 知識必要 |
| 機能網羅 | ◎ | 装飾 / 数式 / グラフ / 画像 / mergeCells すべて対応 |
| ブラウザ対応 | ◎ | Node / browser 同 API |
| バンドル | △ | 200KB 前後、フォントサブセットなどの内部リソース大きい |
| TypeScript | ○ | 型同梱、ただし一部 any |
向く / 向かないケース
- 向く: 帳票出力、レポート PDF と並ぶ業務系出力、ユーザダウンロード機能
- 向かない: シンプルな CSV 出力(Papa.unparse のほうが軽い)
- 向かない: 100 万行超(streaming write or 別ツール)。100,000 行で書き出し 1.8 秒(上の実測)なので、その 10 倍は UI スレッドでは無理
関連 Topic / 関連書籍
この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:
改訂3版JavaScript本格入門
ES2022対応の文法基礎からオブジェクト指向構文まで一冊で体系的に押さえる
実践Node.js入門ー基礎・開発・運用
Node.jsの基礎文法から非同期処理・CLI・Express実装まで一気通貫で学ぶ。フロントエンド開発の経験があり、Node.jsでのサーバーサイド開発を体系的に学び直したい人向け。
プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで
TypeScriptの型システムを基礎から高度な表現力まで体系的に習得する
はじめてのWebデザイン&プログラミング : HTML、CSS、JavaScript、PHPの基本
HTML・CSS・JavaScript・PHP を横断してWebの全体像を最短で把握する
フロントエンドの知識地図ーー 一冊でHTML/CSS/JavaScriptの開発技術が…
フロントエンド技術の全体像を俯瞰し、学習の優先順位を自分で判断できるようにする