請求書 PDF出力(Invoice PDF)— window.print() でA4帳票をPDF化
このコンポーネントについて
業務システムでは担当者が画面から請求書をPDFで出力するケースが多くあります。このページでは、JSONファイルから fetch で取得した請求データをJavaScriptで帳票テンプレートに動的に流し込み、window.print() と印刷用CSSを使ってPDF出力する実装例を紹介します。「PDF出力」ボタンを押すと別タブで印刷ビューが開き、ブラウザの印刷ダイアログから「PDFに保存」を選ぶだけでPDFが生成できます。サーバーへのPDFファイル保存は不要で、バニラJSのみ・CDN不要で完結します。
- JSONデータの fetch 読み込み — 請求データを JSON ファイルから
fetchで取得し、JavaScript で帳票テンプレートに動的に流し込む。データと表示ロジックが分離されるため、請求内容の差し替えが JSON の編集だけで済む - 請求書テンプレート描画 — 請求先・発行元・明細テーブル・合計・振込先を含むA4帳票レイアウトを
escapeHtml()でXSS対策しながら動的に構築 - 別タブで印刷ビュー展開 — 「PDF出力」ボタンで
window.open()を使って新しいタブを開き、印刷専用HTMLを書き込んで表示する - 印刷ダイアログ自動起動 — 新タブのロード完了時に
window.print()を自動発火し、ブラウザの印刷ダイアログをすぐに表示する - A4サイズ指定 —
@page { size: A4; margin: 20mm; }で印刷時の用紙サイズをA4に固定する - CDN不要・バニラJSのみ — 外部ライブラリを一切使わず、コピペして即使えるシンプルな実装
実装のポイント・注意点
window.open('', '_blank') はブラウザのポップアップブロックが有効な場合に null を返します。返り値を必ず if (!printWindow) で確認し、null のときはアラートを表示して処理を中断してください。ユーザーが意図してブロックを解除するまで何も起きないように見えてしまうため、このチェックは必須です。
印刷ダイアログの起動には printWindow.onload を使います。document.close() の直後に print() を呼ぶと、CSSが未適用の状態でダイアログが開く場合があります(Chrome・Edgeで確認済み)。また document.close() を呼ばないと onload が発火しないブラウザがあるため、必ず close() → onload の順で記述してください。
帳票をA4で出力できるのは @page { size: A4; margin: 20mm; } の1行があるためです。この @page ルールは印刷時の用紙サイズと余白をCSSで指定する仕組みで、ブラウザの印刷エンジンがこの指定に合わせてレンダリングします。帳票レイアウト自体は通常のHTML/CSSで作れるので、難しい部分はこの1行だけと覚えておけば十分です。ただし @page { size: A4; } はChrome・Edgeで有効ですが、Safariでは用紙サイズ指定が効かない場合があります。Safariユーザーには印刷ダイアログ側でA4を手動選択してもらう必要があります。印刷用HTMLは静的なテンプレート文字列としてハードコードしてください。変数を直接 document.write() に渡す場合は、必ず escapeHtml() でサニタイズしてXSSを防止してください。
A4帳票のHTMLレイアウトも getPrintHTML() の中にサンプルとして含まれています。この関数をまるごとコピペするだけで帳票レイアウトから印刷呼び出しまで一式揃います。
HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。
デモ
データを読み込み中…
サンプルソース
4つのファイルを同じフォルダに保存し、ローカルサーバー経由で index.html を開いてください。
※ fetch を使うため、ファイルをダブルクリックで直接開くと動作しません。VS Code の Live Server 拡張や python -m http.server などのローカルサーバーが必要です。
ファイル名:index.html / style.css / script.js / data/data.json
— 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>請求書 PDF出力 サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<!-- 請求書プレビューエリア(script.js が data/data.json を取得して描画する) -->
<div class="invoice-wrap">
<div id="invoice-preview" class="invoice-paper">
<p class="invoice-loading">データを読み込み中…</p>
</div>
<div class="invoice-actions">
<button class="invoice-print-btn" id="printBtn" onclick="printInvoice()" disabled>PDF出力</button>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
:root {
--invoice-primary: #2B7FE8;
--invoice-text: #1a2332;
--invoice-text-muted: #5a6a7a;
--invoice-border: #e2e8f0;
--invoice-bg-subtle: #f4f6f9;
}
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: sans-serif;
padding: 24px;
background: #f0f2f5;
color: var(--invoice-text);
}
.invoice-wrap {
max-width: 720px;
margin: 0 auto;
}
/* ===== 帳票カード(紙の見た目) ===== */
.invoice-paper {
background: #fff;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.12);
max-width: 680px;
margin: 0 auto;
padding: 40px;
}
.invoice-loading {
color: #9aa5b4;
font-size: 14px;
padding: 48px 20px;
text-align: center;
margin: 0;
}
/* ===== ヘッダー(タイトル + 番号・日付) ===== */
.invoice-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 32px;
}
.invoice-heading {
margin: 0;
font-size: 28px;
font-weight: 700;
}
.invoice-meta {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 16px;
font-size: 13px;
}
.invoice-meta dt { font-weight: 600; color: #7a8a9a; white-space: nowrap; margin: 0; }
.invoice-meta dd { margin: 0; }
/* ===== 請求先・発行元(2カラム) ===== */
.invoice-parties {
display: flex;
gap: 32px;
margin-bottom: 32px;
flex-wrap: wrap;
}
.invoice-to, .invoice-from { flex: 1; min-width: 200px; }
.invoice-label {
font-size: 11px;
font-weight: 700;
color: var(--invoice-primary);
letter-spacing: 0.06em;
text-transform: uppercase;
margin: 0 0 6px;
}
.invoice-company { font-size: 16px; font-weight: 700; margin: 0 0 4px; }
.invoice-addr { font-size: 13px; color: var(--invoice-text-muted); margin: 0 0 2px; }
/* ===== 明細テーブル ===== */
.invoice-table { width: 100%; border-collapse: collapse; margin-bottom: 24px; font-size: 14px; }
.invoice-table th { background: var(--invoice-primary); color: #fff; padding: 10px 12px; text-align: left; font-weight: 600; }
.invoice-table td { padding: 10px 12px; border-bottom: 1px solid var(--invoice-border); }
.invoice-table .col-qty,
.invoice-table .col-price,
.invoice-table .col-amount { text-align: right; }
.invoice-table tbody tr:last-child td { border-bottom: none; }
/* ===== 合計エリア ===== */
.invoice-totals { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; margin-bottom: 32px; }
.invoice-total-row { display: flex; justify-content: space-between; gap: 48px; font-size: 14px; color: var(--invoice-text-muted); min-width: 240px; }
.invoice-total-row.is-grand { font-size: 18px; font-weight: 700; color: var(--invoice-text); border-top: 2px solid var(--invoice-text); padding-top: 8px; margin-top: 4px; }
/* ===== 振込先 ===== */
.invoice-bank { background: var(--invoice-bg-subtle); border: 1px solid var(--invoice-border); border-radius: 8px; padding: 16px 20px; font-size: 14px; }
.invoice-bank p { margin: 0 0 4px; }
.invoice-bank p:last-child { margin: 0; }
/* ===== PDF出力ボタン ===== */
.invoice-actions { display: flex; justify-content: center; padding: 20px 0 4px; }
.invoice-print-btn {
background: var(--invoice-primary);
color: #fff;
border: none;
padding: 12px 32px;
border-radius: 6px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s;
}
.invoice-print-btn:hover { background: #1A6ED4; }
.invoice-print-btn:disabled { background: #9aa5b4; cursor: not-allowed; }
var invoiceData = null;
// ページ読み込み時に invoice-data.json を取得して帳票を描画する
document.addEventListener('DOMContentLoaded', function () {
fetch('./data/data.json')
.then(function (r) { return r.json(); })
.then(function (data) {
invoiceData = data;
renderInvoice(data);
document.getElementById('printBtn').disabled = false;
})
.catch(function () {
document.getElementById('invoice-preview').innerHTML =
'<p style="color:#ef4444;padding:20px;">データの読み込みに失敗しました。</p>';
});
});
// JSONデータから帳票HTMLを組み立ててDOMに描画する
function renderInvoice(data) {
var rows = data.items.map(function (item) {
return '<tr>' +
'<td>' + esc(item.name) + '</td>' +
'<td class="col-qty">' + esc(item.qty) + '</td>' +
'<td class="col-price">'+ esc(item.unitPrice) + '</td>' +
'<td class="col-amount">'+ esc(item.amount) + '</td>' +
'</tr>';
}).join('');
document.getElementById('invoice-preview').innerHTML =
'<div class="invoice-head">' +
'<h2 class="invoice-heading">請求書</h2>' +
'<dl class="invoice-meta">' +
'<dt>請求書番号</dt><dd>' + esc(data.number) + '</dd>' +
'<dt>発行日</dt><dd>' + esc(data.issuedAt) + '</dd>' +
'<dt>支払期限</dt><dd>' + esc(data.dueDate) + '</dd>' +
'</dl>' +
'</div>' +
'<div class="invoice-parties">' +
'<div class="invoice-to">' +
'<p class="invoice-label">請求先</p>' +
'<p class="invoice-company">' + esc(data.to.company) + '</p>' +
'<p class="invoice-addr">' + esc(data.to.address) + '</p>' +
'</div>' +
'<div class="invoice-from">' +
'<p class="invoice-label">発行元</p>' +
'<p class="invoice-company">' + esc(data.from.company) + '</p>' +
'<p class="invoice-addr">' + esc(data.from.address) + '</p>' +
'<p class="invoice-addr">TEL: ' + esc(data.from.tel) + '</p>' +
'</div>' +
'</div>' +
'<table class="invoice-table">' +
'<thead><tr>' +
'<th class="col-name">品目</th>' +
'<th class="col-qty">数量</th>' +
'<th class="col-price">単価</th>' +
'<th class="col-amount">金額</th>' +
'</tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>' +
'<div class="invoice-totals">' +
'<div class="invoice-total-row"><span>小計</span><span>' + esc(data.subtotal) + '</span></div>' +
'<div class="invoice-total-row"><span>消費税(' + esc(data.taxRate) + ')</span><span>' + esc(data.tax) + '</span></div>' +
'<div class="invoice-total-row is-grand"><span>合計(税込)</span><span>' + esc(data.total) + '</span></div>' +
'</div>' +
'<div class="invoice-bank">' +
'<p class="invoice-label">振込先</p>' +
'<p>' + esc(data.bank) + '</p>' +
'</div>';
}
// PDF出力ボタンのクリックで呼び出す関数
function printInvoice() {
if (!invoiceData) return;
var printWindow = window.open('', '_blank');
if (!printWindow) {
alert('ポップアップがブロックされました。ブラウザの設定を確認してください。');
return;
}
printWindow.document.write(getPrintHTML(invoiceData));
printWindow.document.close();
printWindow.onload = function () { printWindow.print(); };
}
// JSONデータから印刷用HTMLを組み立てる(@page CSS + 帳票HTML)
function getPrintHTML(data) {
var rows = data.items.map(function (item) {
return '<tr><td>' + esc(item.name) + '</td>' +
'<td class="col-qty">' + esc(item.qty) + '</td>' +
'<td class="col-price">' + esc(item.unitPrice) + '</td>' +
'<td class="col-amount">'+ esc(item.amount) + '</td></tr>';
}).join('');
return '<!DOCTYPE html><html lang="ja"><head><meta charset="UTF-8"><style>' +
'@page { size: A4; margin: 20mm; } ' +
'*, *::before, *::after { box-sizing: border-box; } ' +
'body { font-family: sans-serif; color: #1a2332; margin: 0; padding: 0; font-size: 13px; } ' +
'@media screen { body { background: #f0f2f5; padding: 24px; } .inv-page { max-width: 680px; margin: 0 auto; background: #fff; padding: 40px; box-shadow: 0 2px 20px rgba(0,0,0,.12); } } ' +
'h2 { margin: 0 0 28px; font-size: 26px; font-weight: 700; } ' +
'.inv-head { display: flex; justify-content: space-between; margin-bottom: 24px; } ' +
'.inv-meta { display: grid; grid-template-columns: auto 1fr; gap: 3px 12px; font-size: 12px; } ' +
'.inv-meta dt { font-weight: 600; color: #7a8a9a; white-space: nowrap; margin: 0; } ' +
'.inv-meta dd { margin: 0; } ' +
'.inv-parties { display: flex; gap: 24px; margin-bottom: 24px; } ' +
'.inv-lbl { font-size: 10px; font-weight: 700; color: #2B7FE8; letter-spacing: .06em; text-transform: uppercase; margin: 0 0 4px; } ' +
'.inv-company { font-size: 15px; font-weight: 700; margin: 0 0 4px; } ' +
'.inv-addr { font-size: 12px; color: #5a6a7a; margin: 0 0 2px; } ' +
'table { width: 100%; border-collapse: collapse; margin-bottom: 20px; font-size: 13px; } ' +
'th { background: #2B7FE8; color: #fff; padding: 8px 10px; text-align: left; font-weight: 600; } ' +
'td { padding: 8px 10px; border-bottom: 1px solid #e2e8f0; } ' +
'.col-qty, .col-price, .col-amount { text-align: right; } ' +
'.inv-totals { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; margin-bottom: 24px; } ' +
'.inv-total-row { display: flex; justify-content: space-between; gap: 40px; min-width: 220px; font-size: 13px; color: #5a6a7a; } ' +
'.inv-total-row.grand { font-size: 17px; font-weight: 700; color: #1a2332; border-top: 2px solid #1a2332; padding-top: 6px; margin-top: 4px; } ' +
'.inv-bank { background: #f4f6f9; border: 1px solid #e2e8f0; border-radius: 6px; padding: 14px 18px; } ' +
'.inv-bank p { margin: 0 0 3px; font-size: 13px; } ' +
'.inv-bank p:last-child { margin: 0; } ' +
'.inv-bank .inv-lbl { margin-bottom: 8px; } ' +
'</style></head><body><div class="inv-page">' +
'<h2>請求書</h2>' +
'<div class="inv-head"><div></div><dl class="inv-meta">' +
'<dt>請求書番号</dt><dd>' + esc(data.number) + '</dd>' +
'<dt>発行日</dt><dd>' + esc(data.issuedAt) + '</dd>' +
'<dt>支払期限</dt><dd>' + esc(data.dueDate) + '</dd>' +
'</dl></div>' +
'<div class="inv-parties">' +
'<div><p class="inv-lbl">請求先</p>' +
'<p class="inv-company">' + esc(data.to.company) + '</p>' +
'<p class="inv-addr">' + esc(data.to.address) + '</p></div>' +
'<div><p class="inv-lbl">発行元</p>' +
'<p class="inv-company">' + esc(data.from.company) + '</p>' +
'<p class="inv-addr">' + esc(data.from.address) + '</p>' +
'<p class="inv-addr">TEL: ' + esc(data.from.tel) + '</p></div>' +
'</div>' +
'<table><thead><tr>' +
'<th class="col-name">品目</th><th class="col-qty">数量</th>' +
'<th class="col-price">単価</th><th class="col-amount">金額</th>' +
'</tr></thead><tbody>' + rows + '</tbody></table>' +
'<div class="inv-totals">' +
'<div class="inv-total-row"><span>小計</span><span>' + esc(data.subtotal) + '</span></div>' +
'<div class="inv-total-row"><span>消費税(' + esc(data.taxRate) + ')</span><span>' + esc(data.tax) + '</span></div>' +
'<div class="inv-total-row grand"><span>合計(税込)</span><span>' + esc(data.total) + '</span></div>' +
'</div>' +
'<div class="inv-bank"><p class="inv-lbl">振込先</p><p>' + esc(data.bank) + '</p></div>' +
'</div></body></html>';
}
// XSS対策:動的データをHTMLに埋め込む前にエスケープする
function esc(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
{
"number": "INV-2025-0042",
"issuedAt": "2025年6月1日",
"dueDate": "2025年6月30日",
"to": {
"company": "株式会社サンプルシステム 御中",
"address": "東京都渋谷区〇〇 1-2-3"
},
"from": {
"company": "コピペ de UI 株式会社",
"address": "東京都千代田区△△ 4-5-6",
"tel": "03-0000-0000"
},
"items": [
{ "name": "フロントエンド開発支援", "qty": "20h", "unitPrice": "¥8,000", "amount": "¥160,000" },
{ "name": "UIデザインレビュー", "qty": "5h", "unitPrice": "¥10,000", "amount": "¥50,000" },
{ "name": "動作確認・テスト", "qty": "3h", "unitPrice": "¥8,000", "amount": "¥24,000" }
],
"subtotal": "¥234,000",
"taxRate": "10%",
"tax": "¥23,400",
"total": "¥257,400",
"bank": "○○銀行 渋谷支店 普通 1234567 コピペデ ユーアイ(カ"
}
AI用プロンプト
以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。請求データの差し替えや列の追加など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
# 請求書 PDF出力 作成依頼
## 概要
JSONファイルから請求データを fetch で取得し、JavaScriptで帳票テンプレートに動的に流し込んで
window.print() でPDF出力するUIを実装してください。
「PDF出力」ボタンを押すと別タブで印刷ビューが開き、ブラウザの印刷ダイアログでPDFに保存できます。
## 要件
- 請求データは JSON ファイル(invoice-data.json)から fetch で取得すること
- 取得したデータを renderInvoice(data) 関数でDOM描画すること
- XSS対策として、動的データはすべて escapeHtml() でエスケープしてから innerHTML に渡すこと
- 請求先・発行元・明細テーブル・合計・振込先を含む請求書レイアウトを表示すること
- 「PDF出力」ボタンをクリックすると新しいタブが開くこと
- 新しいタブには getPrintHTML(data) が生成したHTMLが書き込まれ、印刷ダイアログが自動で起動すること
- @page { size: A4; margin: 20mm; } で印刷時の用紙サイズをA4に固定すること
- ポップアップがブロックされた場合はアラートで通知すること
- 外部ライブラリは使用しないこと
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:不要(帳票レイアウトは固定幅)
## 動作詳細
DOMContentLoaded で fetch('./invoice-data.json') を実行し、取得したデータを renderInvoice(data) に渡す。
renderInvoice() はデータから帳票HTMLをescapeHtml()でXSS対策しながら組み立てて innerHTML に設定する。
データ取得完了後にPDF出力ボタンを有効化する(disabled 解除)。
printInvoice() は window.open('', '_blank') で新タブを開き、getPrintHTML(data) の返す HTML を document.write() で書き込む。
printWindow.document.close() の後、printWindow.onload で printWindow.print() を呼び出して印刷ダイアログを自動起動する。
## サンプルJSON(invoice-data.json)
{
"number": "INV-2025-0042",
"issuedAt": "2025年6月1日",
"dueDate": "2025年6月30日",
"to": { "company": "株式会社サンプルシステム 御中", "address": "東京都渋谷区〇〇 1-2-3" },
"from": { "company": "コピペ de UI 株式会社", "address": "東京都千代田区△△ 4-5-6", "tel": "03-0000-0000" },
"items": [
{ "name": "フロントエンド開発支援", "qty": "20h", "unitPrice": "¥8,000", "amount": "¥160,000" },
{ "name": "UIデザインレビュー", "qty": "5h", "unitPrice": "¥10,000", "amount": "¥50,000" },
{ "name": "動作確認・テスト", "qty": "3h", "unitPrice": "¥8,000", "amount": "¥24,000" }
],
"subtotal": "¥234,000",
"taxRate": "10%",
"tax": "¥23,400",
"total": "¥257,400",
"bank": "○○銀行 渋谷支店 普通 1234567 コピペデ ユーアイ(カ"
}
## 出力形式
HTML・CSS・JavaScript・JSONを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。