HTMLで美しく設計したテーブルを PDF に変換したとき、ヘッダーが消えて 2ページ目の行が宙に浮いていたり、1行だけが切れてページをまたいでいたり――そんな経験はありませんか?
テーブルは PDF 変換で最もトラブルが多い要素のひとつです。この記事では、FUNBREW PDF API や Chromium ベースのエンジンを使う際に役立つ、テーブルの改ページ制御テクニックを網羅的に解説します。
なぜ PDF でテーブルが切れるのか
ブラウザはページの概念がなくコンテンツをスクロールで表示しますが、PDF は固定高さの「ページ」に分割します。PDF エンジンはコンテンツを上から順に配置し、ページ末尾に到達したら次ページへ移動します。
このとき、テーブルの行が「ちょうどページの境界線にかかる」場合に問題が起きます。
よくある症状:
| 症状 | 原因 |
|---|---|
| ページをまたいだ行が途中で切れる | break-inside 未設定 |
| 2ページ目以降にヘッダー行がない | thead の繰り返し設定が不十分 |
| colspan/rowspan 行で崩れる | 複雑なセル結合と改ページの干渉 |
| 合計行が孤立してページ末尾に残る | フッター行の分割防止未設定 |
CSSによる改ページ制御の基本
break-inside: avoid でテーブルを分割させない
もっともシンプルな解決策は、テーブル全体を1ページ内に収めることです。
/* テーブル全体が1ページに収まるなら、分割させない */
table {
break-inside: avoid;
page-break-inside: avoid; /* 古いエンジン向けフォールバック */
}
ただし、行数が多いテーブルでは1ページに収まらないため、この設定だけでは不十分です。
行単位で分割を防ぐ
テーブルが複数ページにわたる場合、行(<tr>)を途中で切らない設定が基本です。
/* 行が改ページで分割されるのを防ぐ */
tr {
break-inside: avoid;
page-break-inside: avoid;
}
/* Chromiumベースのエンジン(FUNBREW PDFのqualityエンジン)での確実な設定 */
tr {
break-inside: avoid;
page-break-inside: avoid;
-webkit-column-break-inside: avoid;
}
セル内テキストの折り返し制御
セル内のテキストが長すぎると、行の高さが増してページ境界にかかりやすくなります。
td, th {
/* 長い単語を折り返す */
overflow-wrap: break-word;
word-break: break-word;
/* URLやコードが横にはみ出さないようにする */
max-width: 200px;
white-space: normal;
}
thead 繰り返し表示のテクニック
複数ページにわたるテーブルで、2ページ目以降にもヘッダー行を表示するには display: table-header-group を使います。
/* ページをまたぐとき、theadを各ページの先頭に繰り返す */
thead {
display: table-header-group;
}
/* tfoot(合計行など)を最終ページの末尾に固定 */
tfoot {
display: table-footer-group;
}
HTMLマークアップの確認
thead/tfoot の繰り返しが機能するには、HTML の構造が正しい必要があります。
<table>
<!-- theadを必ず明示的に指定 -->
<thead>
<tr>
<th>品目</th>
<th>数量</th>
<th>単価</th>
<th>小計</th>
</tr>
</thead>
<!-- データ行はtbodyに入れる -->
<tbody>
<tr>
<td>Webサイト制作</td>
<td>1</td>
<td>¥300,000</td>
<td>¥300,000</td>
</tr>
<!-- ... -->
</tbody>
<!-- 合計行はtfootに入れると最終ページ末尾に固定 -->
<tfoot>
<tr>
<td colspan="3">合計</td>
<td>¥330,000</td>
</tr>
</tfoot>
</table>
注意:
tfootを使うと合計行が常に最終ページの末尾に表示されます。合計行をデータ行の直後に置きたい場合は、tbody内の最終行として実装し、break-inside: avoidでグループ化する方法(後述)を使ってください。
theadのスタイリング
ページをまたいで繰り返されるヘッダーは、視認性を高めるスタイリングが重要です。
thead tr {
background-color: #1e3a5f;
color: #ffffff;
/* PDFで背景色を出力するために必須 */
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
thead th {
padding: 10px 14px;
font-weight: 700;
font-size: 10pt;
text-align: left;
border-bottom: 2px solid #0f2443;
}
/* 繰り返し表示されるheadの上部に区切り線を追加 */
@media print {
thead {
border-top: 1px solid #e2e8f0;
}
}
長いテーブルの分割戦略
複数の tbody でグループ管理
行数が多い場合、<tbody> を複数に分割して各グループの分割を防ぐのが実用的なパターンです。請求書の月別小計や、カテゴリ別集計に有効です。
<table>
<thead>
<tr>
<th>月</th>
<th>項目</th>
<th>金額</th>
</tr>
</thead>
<!-- 1月分グループ(まとめて次ページに移動させる) -->
<tbody style="break-inside: avoid; page-break-inside: avoid;">
<tr>
<td rowspan="3">1月</td>
<td>開発費</td>
<td>¥500,000</td>
</tr>
<tr>
<td>サーバー費</td>
<td>¥50,000</td>
</tr>
<tr class="subtotal">
<td>1月小計</td>
<td>¥550,000</td>
</tr>
</tbody>
<!-- 2月分グループ -->
<tbody style="break-inside: avoid; page-break-inside: avoid;">
<tr>
<td rowspan="2">2月</td>
<td>開発費</td>
<td>¥480,000</td>
</tr>
<tr>
<td>サーバー費</td>
<td>¥50,000</td>
</tr>
</tbody>
</table>
CSSでまとめて指定する場合:
/* 全tbodyのbreak-insideをまとめて設定 */
tbody {
break-inside: avoid;
page-break-inside: avoid;
}
/* 小計行のスタイル */
tr.subtotal td {
font-weight: 700;
background-color: #f0f4ff;
border-top: 2px solid #c7d7f0;
border-bottom: 2px solid #c7d7f0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
ページ境界での強制分割
逆に、特定の位置で強制的に改ページしたい場合は、ダミー行を使う方法があります。
<!-- 特定のtbodyの前で改ページ -->
<tbody>
<tr class="page-break-row">
<td colspan="4" style="
break-before: page;
page-break-before: always;
padding: 0;
height: 0;
border: none;
"></td>
</tr>
<tr>
<td>新しいページのデータ</td>
<!-- ... -->
</tr>
</tbody>
または、CSS クラスで管理:
/* テーブル内の改ページポイント */
.table-page-break {
break-before: page;
page-break-before: always;
}
/* ダミー行は視覚的に消す */
.table-page-break td {
padding: 0;
height: 0;
border: none;
font-size: 0;
}
複雑なテーブル(colspan / rowspan)の対応
colspan や rowspan を使ったテーブルは、改ページとの組み合わせで特に問題が起きやすいです。
rowspan のある行の分割問題
rowspan="3" の行がページ境界にかかると、結合セルが分断されて表示が崩れます。
問題のあるパターン:
<!-- rowspan行が改ページで切れる例 -->
<tr>
<td rowspan="4">カテゴリA</td> <!-- ←この行がページ末尾に来ると崩れる -->
<td>項目1</td>
<td>¥10,000</td>
</tr>
<tr>
<td>項目2</td>
<td>¥20,000</td>
</tr>
解決策: rowspanグループをtbodyで囲む
<table>
<thead>
<tr>
<th>カテゴリ</th>
<th>項目</th>
<th>金額</th>
</tr>
</thead>
<!-- rowspanグループをtbodyで分割防止 -->
<tbody style="break-inside: avoid;">
<tr>
<td rowspan="4">カテゴリA</td>
<td>項目1</td>
<td>¥10,000</td>
</tr>
<tr><td>項目2</td><td>¥20,000</td></tr>
<tr><td>項目3</td><td>¥15,000</td></tr>
<tr class="subtotal">
<td>小計</td><td>¥45,000</td>
</tr>
</tbody>
<tbody style="break-inside: avoid;">
<tr>
<td rowspan="3">カテゴリB</td>
<td>項目A</td>
<td>¥30,000</td>
</tr>
<tr><td>項目B</td><td>¥25,000</td></tr>
<tr class="subtotal">
<td>小計</td><td>¥55,000</td>
</tr>
</tbody>
</table>
colspanを含むヘッダーの繰り返し
複雑なマルチレベルヘッダーを繰り返す場合:
<table>
<thead>
<!-- 2段のヘッダー行もtheadに含めれば繰り返される -->
<tr>
<th rowspan="2">品目</th>
<th colspan="2">第1四半期</th>
<th colspan="2">第2四半期</th>
</tr>
<tr>
<th>予算</th>
<th>実績</th>
<th>予算</th>
<th>実績</th>
</tr>
</thead>
<tbody>
<!-- ... -->
</tbody>
</table>
/* 複数行theadのスタイル */
thead tr:first-child th {
background-color: #1e3a5f;
color: #ffffff;
text-align: center;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
thead tr:last-child th {
background-color: #2d5585;
color: #ffffff;
font-size: 9pt;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
レスポンシブテーブルの PDF 対応
レスポンシブ設計のテーブルは、画面では横スクロールやカード型表示に切り替わりますが、PDF ではそのまま崩れることがあります。
横スクロールテーブルをPDFで縮小表示
/* 画面では横スクロール */
.table-wrapper {
overflow-x: auto;
}
/* PDF生成時は縮小して1ページ幅に収める */
@media print {
.table-wrapper {
overflow: visible;
}
/* テーブルをページ幅に合わせて縮小 */
.table-responsive {
width: 100%;
font-size: 8pt; /* フォントを小さくして収める */
table-layout: fixed;
}
.table-responsive td,
.table-responsive th {
padding: 5px 6px; /* パディングも縮小 */
word-break: break-word;
}
}
カード型レスポンシブをPDF向けにテーブル表示に戻す
画面ではカード型(display: block)表示のテーブルを、PDF では通常のテーブル表示に戻す方法:
/* 画面ではカード型 */
@media screen and (max-width: 768px) {
table, thead, tbody, th, td, tr {
display: block;
}
thead tr {
display: none; /* 画面ではヘッダー行を隠す */
}
td::before {
content: attr(data-label);
font-weight: 700;
display: inline-block;
width: 40%;
}
}
/* PDFでは通常のテーブル表示に戻す */
@media print {
table { display: table; }
thead { display: table-header-group; }
tbody { display: table-row-group; }
tr { display: table-row; }
th, td { display: table-cell; }
thead tr { display: table-row; } /* ヘッダーを表示 */
td::before { display: none; } /* ラベルプレフィックスを非表示 */
}
列幅の固定とPDF最適化
ブラウザと PDF エンジンでは列幅の計算が異なる場合があります。table-layout: fixed で固定幅を指定すると安定します。
table {
table-layout: fixed;
width: 100%;
}
/* 列幅を明示的に指定 */
.col-item { width: 40%; }
.col-qty { width: 10%; }
.col-price { width: 20%; }
.col-total { width: 20%; }
.col-action { width: 10%; }
<table>
<colgroup>
<col class="col-item">
<col class="col-qty">
<col class="col-price">
<col class="col-total">
</colgroup>
<thead>
<tr>
<th>品目</th>
<th>数量</th>
<th>単価</th>
<th>小計</th>
</tr>
</thead>
<!-- ... -->
</table>
テーブルの完全なスタイリング例
PDF出力で安定して動作するテーブルのCSSをまとめます。
/* === テーブルベーススタイル === */
table {
width: 100%;
border-collapse: collapse;
font-size: 10pt;
line-height: 1.5;
table-layout: fixed;
/* テーブル全体の改ページ制御 */
page-break-inside: auto;
}
/* === ヘッダー === */
thead {
display: table-header-group; /* ページをまたいで繰り返す */
}
thead tr {
background-color: #1e40af;
color: #ffffff;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
thead th {
padding: 10px 12px;
font-weight: 700;
text-align: left;
border: 1px solid #1e3a8a;
font-size: 9.5pt;
}
/* === フッター === */
tfoot {
display: table-footer-group; /* 最終ページ末尾に固定 */
}
tfoot tr {
background-color: #f0f4ff;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
tfoot td {
padding: 10px 12px;
font-weight: 700;
border: 1px solid #c7d2fe;
border-top: 2px solid #4f46e5;
}
/* === データ行 === */
tbody {
/* tbodyを単位とした改ページ防止(グループ化戦略で使用) */
}
tr {
break-inside: avoid;
page-break-inside: avoid;
}
td {
padding: 8px 12px;
border: 1px solid #e2e8f0;
vertical-align: top;
overflow-wrap: break-word;
word-break: break-word;
}
/* 偶数行のストライプ */
tbody tr:nth-child(even) td {
background-color: #f8fafc;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* 小計・合計行 */
tr.subtotal td {
font-weight: 700;
background-color: #eff6ff;
border-top: 2px solid #bfdbfe;
border-bottom: 2px solid #bfdbfe;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* 金額列は右寄せ */
td.amount, th.amount {
text-align: right;
font-variant-numeric: tabular-nums;
}
/* 数値の桁揃え */
td.number {
text-align: right;
font-family: 'Noto Sans JP', monospace;
font-variant-numeric: tabular-nums;
}
実践例:明細付き請求書テーブル
実際の請求書テーブルの完全な実装例です。改ページ対応・thead繰り返し・合計行の固定をすべて含みます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
@page {
size: A4;
margin: 20mm 15mm;
}
body {
font-family: 'Noto Sans JP', 'Hiragino Sans', sans-serif;
font-size: 10pt;
color: #1f2937;
}
/* ===== 請求書ヘッダー ===== */
.invoice-header {
display: flex;
justify-content: space-between;
margin-bottom: 24pt;
break-inside: avoid;
}
.invoice-title {
font-size: 24pt;
font-weight: 700;
color: #1e40af;
}
/* ===== 明細テーブル ===== */
.line-items-table {
width: 100%;
border-collapse: collapse;
font-size: 9.5pt;
table-layout: fixed;
margin-bottom: 0;
}
/* 列幅定義 */
.col-no { width: 6%; }
.col-desc { width: 40%; }
.col-qty { width: 10%; }
.col-unit { width: 18%; }
.col-amount { width: 18%; }
.col-note { width: 8%; }
/* ヘッダー(ページをまたいで繰り返し表示) */
.line-items-table thead {
display: table-header-group;
}
.line-items-table thead tr {
background-color: #1e3a5f;
color: #ffffff;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.line-items-table thead th {
padding: 9px 10px;
font-weight: 700;
border: 1px solid #0f2443;
text-align: left;
}
.line-items-table thead th.align-right {
text-align: right;
}
/* データ行(行単位で分割防止) */
.line-items-table tbody tr {
break-inside: avoid;
page-break-inside: avoid;
}
.line-items-table tbody td {
padding: 7px 10px;
border: 1px solid #d1d5db;
vertical-align: top;
overflow-wrap: break-word;
}
.line-items-table tbody tr:nth-child(even) td {
background-color: #f9fafb;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* 金額列 */
.align-right { text-align: right; }
.font-mono {
font-variant-numeric: tabular-nums;
}
/* カテゴリ区切り行(tbodyで分割防止) */
.line-items-table tbody.category-group {
break-inside: avoid;
}
/* カテゴリヘッダー行 */
.line-items-table tr.category-header td {
background-color: #dbeafe;
font-weight: 700;
font-size: 9pt;
color: #1e3a8a;
padding: 5px 10px;
border-top: 2px solid #93c5fd;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* 小計行 */
.line-items-table tr.subtotal td {
font-weight: 700;
background-color: #eff6ff;
border-top: 2px solid #bfdbfe;
border-bottom: 2px solid #bfdbfe;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* ===== 合計セクション(改ページ分割防止) ===== */
.total-section {
break-inside: avoid;
page-break-inside: avoid;
margin-top: 0;
border: 1px solid #d1d5db;
border-top: none;
}
.total-section table {
width: 100%;
border-collapse: collapse;
}
.total-section td {
padding: 7px 10px;
border-bottom: 1px solid #e5e7eb;
}
.total-section .total-row {
background-color: #1e3a5f;
color: #ffffff;
font-size: 12pt;
font-weight: 700;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.total-section .total-row td {
border-bottom: none;
}
</style>
</head>
<body>
<div class="invoice-header">
<div>
<div class="invoice-title">御請求書</div>
<p>請求番号: INV-2026-0042</p>
<p>発行日: 2026年4月5日</p>
<p>支払期限: 2026年4月30日</p>
</div>
<div style="text-align: right;">
<p style="font-weight: 700; font-size: 12pt;">株式会社サンプル</p>
<p>東京都渋谷区○○1-2-3</p>
</div>
</div>
<!-- 明細テーブル(theadが自動繰り返し) -->
<table class="line-items-table">
<colgroup>
<col class="col-no">
<col class="col-desc">
<col class="col-qty">
<col class="col-unit">
<col class="col-amount">
<col class="col-note">
</colgroup>
<thead>
<tr>
<th class="align-right">No.</th>
<th>品目・摘要</th>
<th class="align-right">数量</th>
<th class="align-right">単価</th>
<th class="align-right">金額</th>
<th>税区分</th>
</tr>
</thead>
<!-- カテゴリ1: 開発費(グループ単位で分割防止) -->
<tbody class="category-group">
<tr class="category-header">
<td colspan="6">■ 開発費</td>
</tr>
<tr>
<td class="align-right">1</td>
<td>Webアプリケーション開発<br><span style="font-size:8.5pt; color:#6b7280;">フロントエンド実装(React)</span></td>
<td class="align-right font-mono">1式</td>
<td class="align-right font-mono">¥800,000</td>
<td class="align-right font-mono">¥800,000</td>
<td class="align-right">課税</td>
</tr>
<tr>
<td class="align-right">2</td>
<td>バックエンドAPI開発<br><span style="font-size:8.5pt; color:#6b7280;">REST API設計・実装(Node.js)</span></td>
<td class="align-right font-mono">1式</td>
<td class="align-right font-mono">¥600,000</td>
<td class="align-right font-mono">¥600,000</td>
<td class="align-right">課税</td>
</tr>
<tr>
<td class="align-right">3</td>
<td>データベース設計・構築</td>
<td class="align-right font-mono">1式</td>
<td class="align-right font-mono">¥200,000</td>
<td class="align-right font-mono">¥200,000</td>
<td class="align-right">課税</td>
</tr>
<tr class="subtotal">
<td colspan="4" class="align-right">開発費 小計</td>
<td class="align-right font-mono">¥1,600,000</td>
<td></td>
</tr>
</tbody>
<!-- カテゴリ2: インフラ費(グループ単位で分割防止) -->
<tbody class="category-group">
<tr class="category-header">
<td colspan="6">■ インフラ・その他費用</td>
</tr>
<tr>
<td class="align-right">4</td>
<td>クラウドサーバー初期設定<br><span style="font-size:8.5pt; color:#6b7280;">AWS EC2/RDS構築、VPC設定</span></td>
<td class="align-right font-mono">1式</td>
<td class="align-right font-mono">¥150,000</td>
<td class="align-right font-mono">¥150,000</td>
<td class="align-right">課税</td>
</tr>
<tr>
<td class="align-right">5</td>
<td>ドメイン・SSL証明書取得</td>
<td class="align-right font-mono">1式</td>
<td class="align-right font-mono">¥30,000</td>
<td class="align-right font-mono">¥30,000</td>
<td class="align-right">課税</td>
</tr>
<tr class="subtotal">
<td colspan="4" class="align-right">インフラ費 小計</td>
<td class="align-right font-mono">¥180,000</td>
<td></td>
</tr>
</tbody>
</table>
<!-- 合計セクション(テーブルと連続させて改ページ防止) -->
<div class="total-section">
<table>
<tr>
<td style="width:70%; text-align:right; color:#6b7280;">小計(税抜)</td>
<td style="width:20%; text-align:right;" class="font-mono">¥1,780,000</td>
<td style="width:10%;"></td>
</tr>
<tr>
<td style="text-align:right; color:#6b7280;">消費税(10%)</td>
<td style="text-align:right;" class="font-mono">¥178,000</td>
<td></td>
</tr>
<tr class="total-row">
<td style="text-align:right;">合計金額(税込)</td>
<td style="text-align:right;" class="font-mono">¥1,958,000</td>
<td></td>
</tr>
</table>
</div>
</body>
</html>
FUNBREW PDF APIでの実装例
上記のHTMLテンプレートを FUNBREW PDF API で実際に PDF 化する方法です。
JavaScript (Node.js)
const fs = require('fs');
// HTMLテンプレートを読み込む
const html = fs.readFileSync('./invoice-template.html', 'utf8');
// 動的データを埋め込む
const invoiceHtml = html
.replace('{{invoice_number}}', 'INV-2026-0042')
.replace('{{issue_date}}', '2026年4月5日')
.replace('{{due_date}}', '2026年4月30日')
.replace('{{customer_name}}', '株式会社テスト')
.replace('{{line_items}}', buildLineItemsHtml(items))
.replace('{{subtotal}}', '1,780,000')
.replace('{{tax}}', '178,000')
.replace('{{total}}', '1,958,000');
// PDF生成API呼び出し
const response = await fetch('https://pdf.funbrew.cloud/api/pdf/generate', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk-your-api-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: invoiceHtml,
options: {
engine: 'quality', // Chromiumエンジン(CSS完全対応)
format: 'A4',
printBackground: true, // 背景色を出力
margin: {
top: '20mm',
bottom: '20mm',
left: '15mm',
right: '15mm',
},
},
}),
});
const result = await response.json();
console.log('PDF生成完了:', result.data.download_url);
Python
import requests
with open('./invoice-template.html', 'r', encoding='utf-8') as f:
html = f.read()
# 動的データを埋め込む
invoice_html = (html
.replace('{{invoice_number}}', 'INV-2026-0042')
.replace('{{issue_date}}', '2026年4月5日')
.replace('{{customer_name}}', '株式会社テスト')
.replace('{{subtotal}}', '1,780,000')
.replace('{{tax}}', '178,000')
.replace('{{total}}', '1,958,000')
)
response = requests.post(
'https://pdf.funbrew.cloud/api/pdf/generate',
headers={'Authorization': 'Bearer sk-your-api-key'},
json={
'html': invoice_html,
'options': {
'engine': 'quality',
'format': 'A4',
'printBackground': True,
'margin': {
'top': '20mm',
'bottom': '20mm',
'left': '15mm',
'right': '15mm',
},
},
},
)
print('PDF生成完了:', response.json()['data']['download_url'])
PHP (Laravel)
$html = file_get_contents(resource_path('views/pdf/invoice.html'));
$invoiceHtml = str_replace(
['{{invoice_number}}', '{{customer_name}}', '{{total}}'],
['INV-2026-0042', '株式会社テスト', '1,958,000'],
$html
);
$response = Http::withToken('sk-your-api-key')
->post('https://pdf.funbrew.cloud/api/pdf/generate', [
'html' => $invoiceHtml,
'options' => [
'engine' => 'quality',
'format' => 'A4',
'printBackground' => true,
'margin' => [
'top' => '20mm',
'bottom' => '20mm',
'left' => '15mm',
'right' => '15mm',
],
],
]);
$downloadUrl = $response->json('data.download_url');
テンプレートエンジンを使った実装
変数置換が多い場合は、FUNBREW PDFのテンプレートエンジンを使うと {{変数名}} の置換を API が自動で行ってくれます。
// テンプレートエンジンAPIを使う場合
const response = await fetch('https://pdf.funbrew.cloud/api/pdf/generate-from-template', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk-your-api-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
template: 'invoice-with-table', // ダッシュボードで登録したテンプレート名
variables: {
invoice_number: 'INV-2026-0042',
issue_date: '2026年4月5日',
customer_name: '株式会社テスト',
items: [
{ no: 1, desc: 'Webアプリ開発', qty: '1式', unit: '800,000', amount: '800,000', tax_type: '課税' },
{ no: 2, desc: 'API開発', qty: '1式', unit: '600,000', amount: '600,000', tax_type: '課税' },
],
subtotal: '1,780,000',
tax: '178,000',
total: '1,958,000',
},
options: { engine: 'quality', format: 'A4' },
}),
});
よくあるトラブルと解決策
| 症状 | 原因 | 解決策 |
|---|---|---|
| ページをまたいだ行が途中で切れる | break-inside 未設定 |
tr { break-inside: avoid; } を追加 |
| 2ページ目以降にヘッダーがない | thead の display 設定 |
thead { display: table-header-group; } |
| 合計行がデータ行と離れた位置に出る | tfoot の挙動 |
tfoot を使わず tbody 末尾の行を break-inside: avoid のグループに含める |
| rowspan行でレイアウトが崩れる | セル結合と改ページの干渉 | rowspanグループを tbody で囲み break-inside: avoid |
| 背景色が PDF に出ない | エンジンの既定設定 | print-color-adjust: exact と printBackground: true を両方設定 |
| テーブルが用紙幅からはみ出す | 固定幅の列 | table-layout: fixed と overflow-wrap: break-word |
| 列幅がブラウザと PDF でずれる | エンジンの列幅計算の差異 | <colgroup> で明示的に列幅を指定 |
| 数字の桁が揃わない | プロポーショナルフォント | font-variant-numeric: tabular-nums を適用 |
デバッグ方法
- Chrome の印刷プレビューで確認:
Ctrl+Shift+P→ "Emulate CSS media type" → "print" で@media printスタイルを確認 - Playground でリアルタイムテスト: HTMLを貼り付けて即 PDF 出力を確認
qualityエンジンを使う: Chromium ベースのqualityエンジンは CSS の改ページプロパティを最も正確に実装しています- 最小再現例から始める: 問題の行だけを含む小さなテーブルで動作確認してから複雑なテーブルに適用
まとめ
PDF テーブルの改ページ問題を解決するための要点:
- 行の分割防止:
tr { break-inside: avoid; }が基本。まずここから始める - thead の繰り返し:
thead { display: table-header-group; }で 2 ページ目以降も自動表示 - グループ管理: 複数
tbody+break-inside: avoidでカテゴリ・rowspan グループを一体管理 - colspan/rowspan: セル結合グループを
tbodyで囲んで分割防止 - レスポンシブ対応:
@media printでテーブル表示に戻し、table-layout: fixedで列幅を安定させる - 背景色:
print-color-adjust: exactとprintBackground: trueを必ずセットで設定 - デバッグ: Chrome 印刷プレビューと Playground で確認
Playground で実際に HTML を試しながら、最適なテーブルレイアウトを見つけてください。テンプレートエンジンと組み合わせた請求書自動化については請求書PDFをAPIで自動生成する方法も参照してください。
関連リンク
- HTML→PDF変換で使えるCSS設計テクニック集 — 改ページ・余白・フォントなどCSS全般のベストプラクティス
- 請求書PDFをAPIで自動生成する方法 — 請求書の自動化実装ガイド
- PDFテンプレートエンジン入門 — 変数・ループ・条件分岐でテーブルを動的生成
- HTML to PDFトラブルシューティング — PDF変換でよくあるエラーと解決策
- wkhtmltopdf vs Chromium — エンジンごとのCSS対応の違い
- PDF API本番環境ガイド — 本番運用でのパフォーマンス最適化
- HTML to PDF完全ガイド — HTML→PDF変換の全体像
- Playground — ブラウザでHTMLをリアルタイムでPDF変換してテスト
- APIリファレンス — エンドポイントの詳細仕様