Old-School Front-End / 2026 ・ No.03

いにしえのフロントエンド入門

第3回 フォーム ── 申請フォームを HTML だけで作り、何が送られるかを見る

公開

CSS を付けていない、HTML だけの申請フォーム。件名、支払い方法のラジオボタン、日付・区分・内容・金額・通貨の3行の明細、領収書のファイル選択、備考、規程の確認のチェックボックス、下書き保存と申請するのボタンが並び、大阪出張の内容が入力されている
今回のゴール。HTML だけで作った申請フォームに、大阪出張の内容を入れたところです(筆者のデモページを、クラウド上の Chromium で撮影。2026年10月17日。ブラウザの枠は説明のために描き足したもの)

AI に頼まず、一行ずつ手で書く「いにしえの」フロントエンド入門。第3回は、業務システムの本丸フォームです。
申請一覧の「+ 新規申請」の先にある申請フォームを、今回も HTML だけで作ります。
入力欄の名前の付け方、部品の選び方、ブラウザがしてくれる入力チェック。そして最後に、実際に送信して、
ボタンを押した瞬間に何が送られているのかを、のぞいてみます。

一

今回のゴール

新井先輩、申請フォームも AI に作ってもらいました! 入力欄の中に薄い字で「件名」って出るの、おしゃれですよね。

古井ほう。で、件名を入力したら、その薄い字はどうなる?

新井消えます。……あれ、じゃあ入力したあとは、何の欄だったか分からない?

古井そういうことだ。今日はフォームの「名札」と「中身」の話をしよう。

いまここ:第3回

1 準備 2 HTML の骨組み 3 フォーム 4 CSS の基本 5 レイアウト 6 Bootstrap 7 JS の基本 8 DOM・イベント 9 入力チェック 10 通信 11 ログイン 12 承認の流れ 13 集計と印刷 14 jQuery 15 仕上げ いまここ 準備 HTML CSS JavaScript 実戦 仕上げ

今回作るのは form.html、申請フォームのページです。中身は次のとおり。

  • 申請の内容 ── 件名(必須)
  • 支払い方法 ── 立て替え(自分で払った)か、会社のカードか
  • 明細 ── 日付・区分・内容・金額・通貨を3行まで(行を増やせるようにするのは第8回)
  • 領収書 ── 画像か PDF。合計3万円以上なら必須(このルールを守らせるのは第9回)
  • 備考と確認 ── 備考の欄、規程を確認したチェック、「下書き保存」「申請する」のボタン
二

フォームが送られるまで

フォームは <form> で入力欄とボタンを包んで作ります。ボタンを押すと、ブラウザは次の順に動きます。

ボタンを押してから、サーバーに届くまで

押す 申請する ブラウザが検査 required・min など name=value にまとめる title=大阪出張 … 1 2 3 止まる メッセージを表示 GET POST URL の後ろ ファイル名だけ 検索向き 本文に入れる ファイルの中身も 申請はこちら 4 サーバーで再検査 ここが本番の関所 5 下書き保存 検査を飛ばす(formnovalidate)
form の属性意味今回の値
action送り先の URL/api/expenses(第10回で用意する、仮のサーバー)
method送り方。get(URL の後ろに付ける)か post(本文に入れる)post
enctype本文の詰め方。ファイルを送るときは multipart/form-data が必要multipart/form-data

使い分けの目安は、データを「見るだけ」なら GET、「登録・変更する」なら POST。検索のように同じ URL で同じ結果を見たいなら GET が便利ですが、申請の登録は POST にします。GET では入力した中身が URL にそのまま残り、ブラウザの履歴にも記録されるからです。

三

名前を付ける:label と name

入力欄には、二種類の「名前」があります。一つは人に見せる名前(label)、もう一つはサーバーに渡す名前(name)です。

label・id・name の関係

<label for="title">件名</label> <input type="text" id="title" name="title"> for と id:人とブラウザのため 名前の文字を押すと、欄が選ばれる 読み上げソフトに欄の名前が伝わる name:サーバーのため 送るときの名前。ないと送られない title=大阪出張 <label><input type="checkbox"> 規程を確認しました</label> ↑ label で包めば for と id は要らない(チェックボックスやラジオボタンでよく使う)

冒頭で新井くんが喜んでいた「入力欄の中の薄い字」は placeholder という属性です。label の代わりに使うと、どうなるでしょう。

二つの小さなフォームを並べた画面。1 placeholder だけの版は、件名を入力すると欄の名前が消えている。2 label を付けた版は、入力しても上に「件名」と表示されたまま
どちらも件名に入力したあと。①は placeholder(薄い文字)が消え、何の欄だったか分かりません。②は label が残ります(筆者のデモページを、クラウド上の Chromium で撮影。2026年10月17日。ブラウザの枠は説明のために描き足したもの)

見た目だけでなく、ブラウザが作るアクセシビリティの木も比べてみました(Playwright で取り出した実際の出力)。

# ① placeholder だけ
- paragraph:
  - textbox "件名": 大阪出張(交通費・宿泊費)
- paragraph:
  - textbox "内容"
- paragraph:
  - spinbutton
- paragraph:
  - button "申請する"

# ② label を付けた版
- paragraph:
  - text: 件名
  - textbox "件名": 大阪出張(交通費・宿泊費)
- paragraph:
  - text: 内容
  - textbox "内容"
- paragraph:
  - text: 金額
  - spinbutton "金額"
- paragraph:
  - button "申請する"

Chrome は placeholder を名前の代わりに使ってくれることもありますが、①の金額の欄(type="number")では名前が消えて、ただの spinbutton になりました。W3C のアクセシビリティの手引きも、placeholder は label の代わりにならないとしています。入力を始めると消える、薄い色で読みにくい、支援技術によって扱いがまちまち、が理由です。

POINT

placeholder は「例:2026年10月 大阪出張」のような入力例のヒントに使い、欄の名前は必ず label で付けましょう。AI が作るフォームには placeholder だけのものがよくあります。「label はある?」は、フォームを読むときの最初の確認ポイントです。

四

入力の部品

入力欄の多くは <input> で、type 属性で種類を切り替えます。経費精算で使う部品を並べました。

フォームの部品

大阪出張 type="text" 1行の文字 件名・内容 14720 type="number" 数 金額 2026-10-15 type="date" 日付 カレンダーで選べる 交通費 select 一覧から一つ 区分・通貨 立て替え 会社のカード type="radio" どれか一つ 支払い方法 確認しました type="checkbox" はい/いいえ 規程の確認 選択 receipt.pdf type="file" ファイル 領収書 textarea 長い文章 備考
部品よく使う属性使いどころのコツ
type="text"maxlength、autocomplete件名・内容。autocomplete でブラウザの自動入力の扱いを指定できる
type="number"min、max、step金額。スマホでは数字のキーボードが出る。step="1" で小数を止める
type="date"min、max日付。カレンダーで選べ、書き方の揺れ(10/15、10月15日…)がなくなる
select と optionvalue区分・通貨。選択肢が多い・決まっているとき。先頭に「選んでください」を置く
type="radio"name、checked支払い方法。同じ name のものが一組になり、どれか一つしか選べない
type="checkbox"value、checked規程の確認。チェックしないと何も送られない(九章で確認)
textarearows、maxlength備考。終了タグがあり、中身が初期値になる
fieldset と legend―関係する欄をひとまとめにして、まとまりに名前を付ける。ラジオボタンの組には特に大事

新井日付も、ふつうの text でよくないですか? 「10/15」って打てばいいし。

古井経理の人が「10/15」「10月15日」「2026.10.15」を並べて集計する姿を想像してみろ。

新井……date にします。

五

明細の表と、ファイル

明細は、前回の table の中に入力欄を入れて作ります。行と列のあるデータなので、表にするのが自然です。

<tr>
  <td><input type="date" name="date" aria-label="1行目の日付" required></td>
  <td><select name="category" aria-label="1行目の区分" required>…</select></td>
  <td><input type="text" name="item" aria-label="1行目の内容" required></td>
  <td><input type="number" name="amount" aria-label="1行目の金額" min="1" step="1" required></td>
  <td><select name="currency" aria-label="1行目の通貨">…</select></td>
</tr>
  • aria-label ── 表の中の入力欄は、横に label を置く場所がない。列の見出しは目に見えているが、読み上げ用の名前を aria-label で付けておく(「1行目の金額」など)
  • name は3行とも同じ ── name="amount" が3つあると、サーバーには amount が3つ並んで届く。何行目かは順番で分かる(九章で実物を見る)
  • required は1行目だけ ── 明細は最低1行。2・3行目は空でもよい

領収書は type="file" です。

<input type="file" id="receipt" name="receipt" accept="image/*,.pdf" multiple>

accept はファイルを選ぶ画面に出す種類の絞り込み(画像と PDF)、multiple は複数のファイルをまとめて選べるようにする指定です。ファイルの中身を送るには、form に method="post" と enctype="multipart/form-data" が両方必要です。片方が欠けると、届くのはファイル名だけになります。

六

ボタンの種類

ボタンは <button> で作ります。type で働きが変わります。

button の type

申請する type="submit" フォームを送る type を省くとこれになる 行を追加 type="button" 押しても何も起きない JS で動かす(第8回) リセット type="reset" 入力を全部消す 押し間違えたら悲劇 下書き保存のひと工夫 <button type="submit" name="action" value="draft" formnovalidate> formnovalidate を付けたボタンで送ると、必須の欄が空でも検査を飛ばして送れる

今回のフォームには、送信ボタンが二つあります。どちらも name="action" で、value が draft(下書き保存)と submit(申請する)。押したボタンの value だけが送られるので、サーバーは「下書きか、申請か」を見分けられます。下書き保存には formnovalidate を付けて、必須の欄が空でも保存できるようにしました。

古井昔は、どのフォームにも「リセット」ボタンが付いていたもんだ。

新井明細を10行入れたあとに押したら……。

古井そう、すべて無に帰す。いにしえの罠だ。業務システムには置かない。

七

申請フォームを組み立てる

ここまでの部品を組み立てた form.html の全体です。header と footer は前回の index.html と同じものを使っています。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>新規申請 | 経費精算システム</title>
</head>
<body>
  <header>
    <p><a href="/"><img src="img/logo.svg" alt="" width="32" height="32"> 経費精算システム</a></p>
    <nav>
      <ul>
        <li><a href="/">申請一覧</a></li>
        <li><a href="approve.html">承認</a></li>
        <li><a href="dashboard.html">ダッシュボード</a></li>
      </ul>
    </nav>
    <p>新井 太郎(申請者)</p>
  </header>

  <main>
    <h1>新規申請</h1>
    <p><a href="/">← 申請一覧へ戻る</a></p>

    <form action="/api/expenses" method="post" enctype="multipart/form-data">
      <fieldset>
        <legend>申請の内容</legend>
        <p>
          <label for="title">件名(必須)</label><br>
          <input type="text" id="title" name="title" required maxlength="50">
        </p>
      </fieldset>

      <fieldset>
        <legend>支払い方法</legend>
        <label><input type="radio" name="payment" value="personal" checked> 立て替え(自分で払った)</label>
        <label><input type="radio" name="payment" value="corporate"> 会社のカード</label>
      </fieldset>

      <fieldset>
        <legend>明細</legend>
        <table>
          <caption>明細(3行まで。行を増やすのは第8回)</caption>
          <thead>
            <tr><th scope="col">日付</th><th scope="col">区分</th><th scope="col">内容</th><th scope="col">金額</th><th scope="col">通貨</th></tr>
          </thead>
          <tbody>
            <tr>
              <td><input type="date" name="date" aria-label="1行目の日付" required></td>
              <td><select name="category" aria-label="1行目の区分" required><option value="">選んでください</option><option value="transport">交通費</option><option value="hotel">宿泊費</option><option value="meeting">会議・交際費</option><option value="supplies">消耗品</option><option value="other">その他</option></select></td>
              <td><input type="text" name="item" aria-label="1行目の内容" required></td>
              <td><input type="number" name="amount" aria-label="1行目の金額" min="1" step="1" required></td>
              <td><select name="currency" aria-label="1行目の通貨"><option value="JPY">円</option><option value="USD">ドル</option></select></td>
            </tr>
            <tr>
              <td><input type="date" name="date" aria-label="2行目の日付"></td>
              <td><select name="category" aria-label="2行目の区分"><option value="">選んでください</option><option value="transport">交通費</option><option value="hotel">宿泊費</option><option value="meeting">会議・交際費</option><option value="supplies">消耗品</option><option value="other">その他</option></select></td>
              <td><input type="text" name="item" aria-label="2行目の内容"></td>
              <td><input type="number" name="amount" aria-label="2行目の金額" min="1" step="1"></td>
              <td><select name="currency" aria-label="2行目の通貨"><option value="JPY">円</option><option value="USD">ドル</option></select></td>
            </tr>
            <tr>
              <td><input type="date" name="date" aria-label="3行目の日付"></td>
              <td><select name="category" aria-label="3行目の区分"><option value="">選んでください</option><option value="transport">交通費</option><option value="hotel">宿泊費</option><option value="meeting">会議・交際費</option><option value="supplies">消耗品</option><option value="other">その他</option></select></td>
              <td><input type="text" name="item" aria-label="3行目の内容"></td>
              <td><input type="number" name="amount" aria-label="3行目の金額" min="1" step="1"></td>
              <td><select name="currency" aria-label="3行目の通貨"><option value="JPY">円</option><option value="USD">ドル</option></select></td>
            </tr>
          </tbody>
        </table>
      </fieldset>

      <fieldset>
        <legend>領収書</legend>
        <p>
          <label for="receipt">領収書の画像または PDF(合計3万円以上は必須)</label><br>
          <input type="file" id="receipt" name="receipt" accept="image/*,.pdf" multiple>
        </p>
      </fieldset>

      <p>
        <label for="note">備考</label><br>
        <textarea id="note" name="note" rows="3" cols="40" maxlength="500"></textarea>
      </p>

      <p>
        <label><input type="checkbox" name="agree" value="yes" required> 経費規程を確認しました(必須)</label>
      </p>

      <p>
        <button type="submit" name="action" value="draft" formnovalidate>下書き保存</button>
        <button type="submit" name="action" value="submit">申請する</button>
      </p>
    </form>
  </main>

  <footer>
    <p><small>© 2026 KIJI 経費精算システム(練習用)</small></p>
  </footer>
</body>
</html>
申請フォームに赤枠と番号を付けたもの。1 申請の内容の fieldset、2 件名の入力欄、3 支払い方法のラジオボタン、4 明細の表、5 領収書のファイル選択、6 備考の textarea、7 規程の確認のチェックボックス、8 下書き保存と申請するのボタン
完成した申請フォームと、使った部品。①fieldset、②text、③radio、④入力欄の入った table、⑤file、⑥textarea、⑦checkbox、⑧button(筆者のデモページを、クラウド上の Chromium で撮影。2026年10月17日。ブラウザの枠は説明のために描き足したもの)

前回の申請一覧の「+ 新規申請」のリンク先が、このページです。一覧から行き来できるようになりました。

八

ブラウザの検査を試す

何も入力せずに「申請する」を押すと、ブラウザが最初の空の欄に吹き出しを出して、送信を止めます。各欄のメッセージを、ブラウザから取り出してみました(日本語設定の Chromium の実際の文言)。

件名(必須)  →  このフィールドを入力してください。
1行目の日付  →  このフィールドを入力してください。
1行目の区分  →  リスト内の項目を選択してください。
1行目の内容  →  このフィールドを入力してください。
1行目の金額  →  このフィールドを入力してください。
経費規程を確認しました(必須)  →  次に進むにはこのチェックボックスをオンにしてください。
金額に 0  →  値は 1 以上にする必要があります。
金額に 12.5  →  有効な値を入力してください。有効な値として最も近いのは 12 と 13 です。

JavaScript を1行も書いていないのに、これだけのチェックが働きます。HTML だけでできる検査は次のとおりです。

HTML の検査と、その限界

required 空のままを止める min / max 範囲の外を止める step 刻みに合わない数を止める maxlength 長すぎる文字を入れさせない pattern 形に合わないものを止める accept 選ぶ画面のファイルを絞る ブラウザの検査 =親切な案内係 すぐ教える。でも外せる サーバーの検査 =金庫の鍵 必ずやる。最後の砦 ・ブラウザの検査は、開発者ツールで属性を消せば簡単に外れる ・「3万円以上なら領収書が必須」など、ほかの欄で決まるルールは第9回で ・accept は選ぶ画面の絞り込みだけ。ほかの種類のファイルが届くこともある ・本物のシステムでは、サーバーで同じルールをもう一度確かめる
NOTE

ブラウザの検査は利用者への親切であって、守りではありません。開発者ツールで required を消せば、空のまま送れてしまいます。セキュリティの手引きとして定番の OWASP も「クライアント側の検査は回避できる。サーバーでも必ず検査を」としています。AI に「入力チェックを付けて」と頼んで画面側だけに付いた場合も同じです。お金を扱うシステムでは、サーバー側の検査が本番だと覚えておきましょう。

九

何が送られるのか

最後に、ボタンを押した瞬間に何が送られているのかを見てみます。まず、送り方を GET に変えて送ったときの URL です(送り先を /echo に変えて試した実際の URL。前半だけ表示し、下はそれを読める形に戻したもの)。

/echo?title=%E5%A4%A7%E9%98%AA%E5%87%BA%E5%BC%B5%EF%BC%88%E4%BA%A4%E9%80%9A%E8%B2%BB%E3%83%BB%E5…

title = 大阪出張(交通費・宿泊費)
payment = personal
date = 2026-10-15
category = transport
item = 新幹線(東京→新大阪)
amount = 14720
currency = JPY
…(2・3行目も date・category・item・amount・currency の同じ名前で続く)…
receipt = receipt.pdf
note = 取引先A社との打ち合わせのため
agree = yes
action = submit

name と、入力した値が name=value の組になって、URL の後ろに & でつながっています。日本語は %E5%A4… のような形に変換(URL エンコード)されます。そして、receipt は「receipt.pdf」という名前だけ。GET ではファイルの中身は送れません。

次に、本来の POST(multipart/form-data)で送ったときに、サーバーに届いた本文です。クラウド上に立てた、受け取った中身を記録するだけの小さなサーバーで受けました(長い部分は省略)。

POST /api/expenses HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBo…

------WebKitFormBoundarykG…
Content-Disposition: form-data; name="title"

大阪出張(交通費・宿泊費)
------WebKitFormBoundarykG…
Content-Disposition: form-data; name="payment"

personal
…(明細の date・category・item・amount・currency が3行ぶん続く)…
------WebKitFormBoundarykG…
Content-Disposition: form-data; name="receipt"; filename="receipt.pdf"
Content-Type: application/pdf

%PDF-1.4
% demo receipt
%%EOF
------WebKitFormBoundarykG…
Content-Disposition: form-data; name="note"

取引先A社との打ち合わせのため
------WebKitFormBoundarykG…
Content-Disposition: form-data; name="agree"

yes
------WebKitFormBoundarykG…
Content-Disposition: form-data; name="action"

submit
------WebKitFormBoundarykG…--

欄ごとに区切り線(boundary)で仕切られ、receipt の部分にはファイルの中身そのものが入っています。最後に、何も入力せずに「下書き保存」を押したときに届いたものです(読める形に整理)。

title = (空)
payment = personal
date = (空)
category = (空)
item = (空)
amount = (空)
currency = JPY
receipt = (空)
note = (空)
action = draft
…明細の2〜3行目も空のまま。agree(チェックボックス)は送られていない
  • 検査を飛ばして届いた ── formnovalidate のおかげで、必須の欄が空でも送られた。サーバーは action=draft を見て「下書きなので空でもよい」と判断する
  • 空の欄も「空」として届く ── title や amount は、値が空のまま名前だけ届く
  • チェックしていないチェックボックスは、名前ごと届かない ── agree が丸ごと消えている。サーバーは「agree がない=チェックされていない」と読む必要がある

新井ボタン一つで、こんなものが飛んでいたんですね……。

古井中身を知っていれば、サーバー側の人とも話が通じる。「agree が来ないんですけど」と言われても、もう慌てないだろう?

結 — 一枚まとめと次回予告

  1. フォームは form で包み、action(送り先)と method(送り方)を決める。登録は POST、ファイルを送るなら enctype="multipart/form-data"。
  2. 入力欄には label(人に見せる名前)と name(サーバーに渡す名前)を付ける。placeholder は label の代わりにならない。
  3. 部品は中身に合った type を選ぶ。金額は number、日付は date、一つを選ぶなら radio か select。関係する欄は fieldset でまとめる。
  4. required・min・step などでブラウザが入力をチェックしてくれる。ただしそれは親切であって守りではない。サーバーでも必ず検査する。
  5. 送られるのは name=value の組。空の欄は空で届き、チェックしていないチェックボックスは届かない。下書き保存は formnovalidate で検査を飛ばす。

新井HTML の骨組み、これで一通りそろいました! でも、見た目がまだ平成どころか昭和ですね……。

古井次回からいよいよ CSS、服を着せる番だ。第4回はセレクタとボックス。一覧を自前の CSS で整えるぞ。

参考

ページの写真は、筆者が作ったデモページを2026年10月17日にクラウド上の Chromium(Playwright)で撮影したものです。検証メッセージは日本語設定の Chromium から取り出した実際の文言、GET の URL・POST の本文・下書き保存の中身は、実際にフォームを送信して記録したものです(POST はクラウド上に立てた記録用の小さなサーバーで受信)。アクセシビリティの木は Playwright の ariaSnapshot() の出力です。ブラウザによって検証メッセージの文言や placeholder の扱いは異なります。登場人物は架空の人物です。

いにしえのフロントエンド入門 第3回 — フォーム:申請フォームを HTML だけで作り、何が送られるかを見る / 2026.10

← 記事一覧へ