帳票テーブルレイアウト — 見積書テンプレート
このコンポーネントについて
見積書・納品書・検収書といった業務帳票の多くは「ヘッダー情報+明細テーブル+合計行」という共通の構造を持っています。このページでは、見積書を題材にその構造をHTML/CSSでテンプレート化した実装例を紹介します。明細行は「追加」「削除」で増減させることができ、行数が変わっても小計・消費税・合計の行が常にテーブル最下部に固定され、列幅や罫線が崩れないことを確認できます。印刷・PDF出力の機能は持たず、あくまで画面表示用の構造テンプレートとして、見積書以外の帳票にも流用しやすい形にしています。金額は自動計算せずダミーの固定値として扱っており、自動計算まで必要な場合は明細行の合計自動計算フォームの事例と組み合わせる想定です。
- 帳票の構造テンプレート — ヘッダー情報(見積番号・発行日・有効期限・宛先・発行元)→明細テーブル→小計・消費税・合計行という、業務帳票に共通するCSS構造をそのまま流用できる形で提供する
- 明細行の追加・削除 — 「明細行を追加」ボタンで行を増やし、各行の削除ボタン(×)で行を減らせる。行数が変わっても合計行の位置・列幅・罫線が崩れない
- 合計行の最下部固定 —
<tfoot>を使って小計・消費税・合計行をテーブル本体と分離し、明細行(<tbody>)がいくつ増減しても常に最下部に表示される - 印刷機能は非搭載 — あえて
window.print()や印刷用CSSを持たせず、画面表示の構造テンプレートに絞っている。印刷・PDF出力が必要な場合は請求書 PDF出力の事例を参照する - CDN不要・バニラJSのみ — 外部ライブラリを一切使わず、コピペして即使えるシンプルな実装
実装のポイント・注意点
明細テーブルは<thead>に見出し行、<tbody>に明細行、<tfoot>に小計・消費税・合計行を分けて配置しています。この3分割により、tbody内の行数がJSで増減してもtfootの位置はテーブル構造上つねに最下部に固定され、「行を足したら合計行が真ん中に埋もれる」といった崩れが起きません。列幅は<colgroup>とtable-layout: fixedで固定し、品目名が長くなっても他の列(数量・単価・金額)の幅が変わらないようにするのがポイントです。なお本事例の金額はすべてデモ用の固定値で、行を追加・削除しても小計・合計は自動再計算されません。実務で自動計算まで組み込みたい場合は、inputイベントでの再計算・整数円での計算を行う「明細行の合計自動計算フォーム」の実装と組み合わせてください。
HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。
デモ
御見積書
宛先
株式会社サンプル商事 御中
発行元
コピペ de UI 株式会社
東京都千代田区△△ 4-5-6
| 品目 | 数量 | 単価 | 金額 | |
|---|---|---|---|---|
| 小計 | ¥448,000 | |||
| 消費税(10%) | ¥44,800 | |||
| 合計(税込) | ¥492,800 | |||
※ 金額はデモ用の固定値です。明細行を追加・削除しても自動では再計算されません。自動計算まで実装したい場合は明細行の合計自動計算フォームを参照してください。
サンプルソース
3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js
— 保存時の文字コードは 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>帳票テーブルレイアウト サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="report-wrap">
<div class="report-paper">
<div class="report-head">
<h2 class="report-heading">御見積書</h2>
<dl class="report-meta">
<dt>見積番号</dt><dd>EST-2026-0031</dd>
<dt>発行日</dt><dd>2026年7月20日</dd>
<dt>有効期限</dt><dd>2026年8月19日</dd>
</dl>
</div>
<div class="report-parties">
<div class="report-to">
<p class="report-label">宛先</p>
<p class="report-company">株式会社サンプル商事 御中</p>
</div>
<div class="report-from">
<p class="report-label">発行元</p>
<p class="report-company">コピペ de UI 株式会社</p>
<p class="report-addr">東京都千代田区△△ 4-5-6</p>
</div>
</div>
<table class="report-table">
<colgroup>
<col class="col-name">
<col class="col-qty">
<col class="col-price">
<col class="col-amount">
<col class="col-action">
</colgroup>
<thead>
<tr>
<th class="col-name">品目</th>
<th class="col-qty">数量</th>
<th class="col-price">単価</th>
<th class="col-amount">金額</th>
<th class="col-action" aria-label="操作"></th>
</tr>
</thead>
<tbody id="reportItemsBody"><!-- 行は script.js が生成 --></tbody>
<tfoot>
<tr class="report-total-row">
<td colspan="3">小計</td>
<td class="col-amount">¥448,000</td>
<td></td>
</tr>
<tr class="report-total-row">
<td colspan="3">消費税(10%)</td>
<td class="col-amount">¥44,800</td>
<td></td>
</tr>
<tr class="report-total-row is-grand">
<td colspan="3">合計(税込)</td>
<td class="col-amount">¥492,800</td>
<td></td>
</tr>
</tfoot>
</table>
<p class="report-note">
※ 金額はデモ用の固定値です。明細行を追加・削除しても自動では再計算されません。
</p>
</div>
<div class="report-controls">
<button class="report-add-btn" id="reportAddBtn" type="button">+ 明細行を追加</button>
<button class="report-reset-btn" id="reportResetBtn" type="button">リセット</button>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: sans-serif;
padding: 24px;
background: #f0f2f5;
color: #1a2332;
}
/* ===== 見積書ラッパー ===== */
.report-wrap {
width: 100%;
max-width: 720px;
margin: 0 auto;
}
/* ===== 帳票カード(紙の見た目) ===== */
.report-paper {
background: #fff;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.12);
padding: 40px;
}
.report-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 32px;
}
.report-heading {
margin: 0;
font-size: 28px;
font-weight: 700;
color: #1a2332;
}
.report-meta {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 16px;
font-size: 13px;
}
.report-meta dt {
font-weight: 600;
color: #7a8a9a;
white-space: nowrap;
margin: 0;
}
.report-meta dd {
margin: 0;
color: #1a2332;
}
.report-parties {
display: flex;
gap: 32px;
margin-bottom: 32px;
flex-wrap: wrap;
}
.report-to,
.report-from {
flex: 1;
min-width: 180px;
}
.report-label {
font-size: 11px;
font-weight: 700;
color: #2B7FE8;
letter-spacing: 0.06em;
text-transform: uppercase;
margin: 0 0 6px;
}
.report-company {
font-size: 16px;
font-weight: 700;
color: #1a2332;
margin: 0 0 4px;
}
.report-addr {
font-size: 13px;
color: #5a6a7a;
margin: 0 0 2px;
}
/* ===== 明細テーブル ===== */
.report-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
margin-bottom: 4px;
font-size: 14px;
}
.report-table .col-name { width: 36%; }
.report-table .col-qty { width: 12%; }
.report-table .col-price { width: 20%; }
.report-table .col-amount { width: 20%; }
.report-table .col-action { width: 12%; }
.report-table th {
background: #2B7FE8;
color: #fff;
padding: 10px 12px;
text-align: left;
font-weight: 600;
}
.report-table td {
padding: 10px 12px;
border-bottom: 1px solid #e2e8f0;
color: #1a2332;
}
.report-table .col-qty,
.report-table .col-price,
.report-table .col-amount {
text-align: right;
}
.report-table .col-action {
text-align: center;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
/* 明細0行時の表示 */
.report-empty-row td {
text-align: center;
color: #9aa5b4;
padding: 24px 12px;
}
/* 行削除ボタン */
.report-row-delete-btn {
background: none;
border: none;
color: #9aa5b4;
font-size: 16px;
line-height: 1;
padding: 4px 8px;
cursor: pointer;
font-family: sans-serif;
transition: color 0.15s;
}
.report-row-delete-btn:hover {
color: #ef4444;
}
/* ===== 合計エリア(tfoot・常に最下部固定) ===== */
.report-table tfoot .report-total-row td {
padding: 10px 12px;
border-bottom: none;
border-top: 1px solid #e2e8f0;
color: #5a6a7a;
}
.report-table tfoot .report-total-row td[colspan] {
text-align: right;
padding-right: 16px;
}
.report-table tfoot .report-total-row.is-grand td {
font-size: 16px;
font-weight: 700;
color: #1a2332;
border-top: 2px solid #1a2332;
padding-top: 12px;
}
.report-note {
font-size: 12px;
line-height: 1.6;
color: #5a6a7a;
margin: 12px 0 0;
}
/* ===== 操作ボタン ===== */
.report-controls {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.report-add-btn {
padding: 8px 20px;
font-size: 14px;
font-weight: 600;
color: #fff;
background: #2B7FE8;
border: none;
border-radius: 6px;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s;
}
.report-add-btn:hover {
background: #1A6ED4;
}
.report-reset-btn {
padding: 6px 16px;
font-size: 13px;
color: #5A6A7A;
background: #fff;
border: 1.5px solid #D0D7E0;
border-radius: 6px;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s, border-color 0.15s;
}
.report-reset-btn:hover {
background: #F4F6F9;
border-color: #9AA5B4;
}
// 見積書の明細データ(サンプル)。金額は自動計算せず固定値として保持する
var INITIAL_ITEMS = [
{ id: 1, name: 'Webサイト制作一式', qty: 1, unitPrice: 400000, amount: 400000 },
{ id: 2, name: 'サーバー保守(月額 × 3ヶ月)', qty: 3, unitPrice: 15000, amount: 45000 },
{ id: 3, name: 'ドメイン取得費用', qty: 1, unitPrice: 3000, amount: 3000 }
];
var reportItems = [];
var nextItemId = 1;
var tbody = document.getElementById('reportItemsBody');
// 金額を「¥12,345」形式の文字列に変換する(表示専用。小計・合計の自動計算はしない)
function formatYen(num) {
return '¥' + num.toLocaleString('ja-JP');
}
// reportItems の内容から明細テーブルの行を再描画する(innerHTMLは使わず createElement で組み立てる)
function renderReportRows() {
tbody.textContent = '';
if (reportItems.length === 0) {
var emptyTr = document.createElement('tr');
emptyTr.className = 'report-empty-row';
var emptyTd = document.createElement('td');
emptyTd.colSpan = 5;
emptyTd.textContent = '明細行がありません';
emptyTr.appendChild(emptyTd);
tbody.appendChild(emptyTr);
return;
}
reportItems.forEach(function (item) {
var tr = document.createElement('tr');
tr.setAttribute('data-id', item.id);
var nameTd = document.createElement('td');
nameTd.textContent = item.name;
tr.appendChild(nameTd);
var qtyTd = document.createElement('td');
qtyTd.className = 'col-qty';
qtyTd.textContent = item.qty;
tr.appendChild(qtyTd);
var priceTd = document.createElement('td');
priceTd.className = 'col-price';
priceTd.textContent = formatYen(item.unitPrice);
tr.appendChild(priceTd);
var amountTd = document.createElement('td');
amountTd.className = 'col-amount';
amountTd.textContent = formatYen(item.amount);
tr.appendChild(amountTd);
// 削除ボタン(この行だけを reportItems から取り除いて再描画する)
var actionTd = document.createElement('td');
actionTd.className = 'col-action';
var deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'report-row-delete-btn';
deleteBtn.setAttribute('aria-label', 'この行を削除');
deleteBtn.textContent = '×';
deleteBtn.addEventListener('click', function () {
reportItems = reportItems.filter(function (i) { return i.id !== item.id; });
renderReportRows();
});
actionTd.appendChild(deleteBtn);
tr.appendChild(actionTd);
tbody.appendChild(tr);
});
}
// 「明細行を追加」:ダミーの固定値で1行追加する(小計・消費税・合計は再計算しない)
function addReportRow() {
reportItems.push({
id: nextItemId,
name: '新しい品目',
qty: 1,
unitPrice: 10000,
amount: 10000
});
nextItemId += 1;
renderReportRows();
}
document.getElementById('reportAddBtn').addEventListener('click', addReportRow);
// リセット:初期の3行に戻す
function resetDemo() {
reportItems = INITIAL_ITEMS.map(function (item) {
return {
id: item.id,
name: item.name,
qty: item.qty,
unitPrice: item.unitPrice,
amount: item.amount
};
});
nextItemId = INITIAL_ITEMS.length + 1;
renderReportRows();
}
document.getElementById('reportResetBtn').addEventListener('click', resetDemo);
// 初期描画
resetDemo();
AI用プロンプト
以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。帳票の種類変更や列の追加など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
# 帳票テーブルレイアウト(見積書テンプレート) 作成依頼
## 概要
見積書を題材にした「ヘッダー情報+明細テーブル+合計行」というHTML/CSSの構造テンプレートを実装してください。
明細行を追加・削除しても、小計・消費税・合計行がテーブル最下部に固定されたまま崩れないことが目的です。
## 要件
- 見積書のヘッダー情報(見積番号・発行日・有効期限・宛先・発行元)を表示すること
- 明細テーブル(品目・数量・単価・金額の4列)を初期3行で表示すること
- テーブル最下部に小計・消費税(10%)・合計の3行を表示すること(金額はダミーの固定値でよく、自動計算は不要)
- 「明細行を追加」ボタンで明細テーブルの最終行(合計行の直前)に新しい行を追加できること
- 各明細行に削除ボタンを付け、押すとその行だけが削除できること
- 行を追加・削除しても、合計行の位置・列幅・罫線が崩れないこと(`<tfoot>`や`table-layout: fixed`の活用を検討すること)
- window.print() や印刷用CSSなど、印刷・PDF出力の機能は実装しないこと
- 外部ライブラリは使用しないこと
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要(スマホ幅では横スクロールでテーブルを閲覧できること)
## 動作詳細
「明細行を追加」ボタンのクリックで、ダミーの行データ(品目名「新しい品目」・数量1・単価10,000円・金額10,000円)を明細テーブルの末尾に追加する。
各行の削除ボタンをクリックすると、その行だけを明細テーブルから削除する。
小計・消費税・合計の行はHTMLに固定値として記述し、行の追加・削除に応じたJSでの再計算は行わない。
明細行のHTML生成は`createElement`で行い、`innerHTML`に変数を直接結合しないこと。
サンプルデータ:
- 見積番号: EST-2026-0031 / 発行日: 2026年7月20日 / 有効期限: 2026年8月19日
- 宛先: 株式会社サンプル商事(御中) / 発行元: コピペ de UI 株式会社(東京都千代田区△△ 4-5-6)
- 明細: Webサイト制作一式 1式 ¥400,000 ¥400,000 / サーバー保守(月額×3ヶ月) 3 ¥15,000 ¥45,000 / ドメイン取得費用 1 ¥3,000 ¥3,000
- 小計: ¥448,000 / 消費税(10%): ¥44,800 / 合計(税込): ¥492,800
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。