本番環境で見かける HTML フォームのほとんどは、textpasswordsubmit の3種類の input しか使っていません。それだけです。しかし HTML には何年も前からリッチな組み込み input タイプ、制約バリデーション、アクセシビリティ機能が備わっています — JavaScript も npm install も不要で、すべて利用可能です。これらを活用しましょう。

知っておくべき input タイプ

各 input タイプは実際の作業を代行してくれます:適切なモバイルキーボードを起動し、組み込みバリデーションを提供し、支援技術に意図を伝えます。実際のプロジェクトで頻繁に登場するものを紹介します:

html
<!-- Email: validates format, shows email keyboard on mobile -->
<input type="email" name="email" autocomplete="email">

<!-- Phone: shows numeric keypad on mobile -->
<input type="tel" name="phone" autocomplete="tel" pattern="[0-9]{10,15}">

<!-- Number: spin buttons, min/max/step validation -->
<input type="number" name="quantity" min="1" max="99" step="1" value="1">

<!-- Date: native date picker (no library needed) -->
<input type="date" name="birthdate" min="1900-01-01" max="2026-12-31">

<!-- Range: slider with min/max/step -->
<input type="range" name="volume" min="0" max="100" step="5" value="50">

<!-- Color: native color picker -->
<input type="color" name="theme_color" value="#0066cc">

<!-- File: single or multiple file upload -->
<input type="file" name="resume" accept=".pdf,.doc,.docx">
<input type="file" name="photos" accept="image/*" multiple>
プロのヒント: type="date" はユーザーのロケールに関係なく YYYY-MM-DD 形式で値を返します — サーバーに送信する際にはまさにそれが必要なものです。可能であれば type="text" の input から日付をパースするのは避けましょう。

label の関連付け — 正しく行う

label はオプションの装飾ではありません。スクリーンリーダーが input を識別する主要な手段であり、label をクリックすると関連する input にフォーカスが移る必要があります。2つの有効なアプローチがあります:

html
<!-- Method 1: for/id association (most common, most flexible) -->
<label for="email">Email address</label>
<input type="email" id="email" name="email">

<!-- Method 2: wrapping label (no id needed) -->
<label>
  Email address
  <input type="email" name="email">
</label>

<!-- Wrong: placeholder is NOT a label -->
<input type="email" name="email" placeholder="Email address">
<!-- placeholder disappears when the user starts typing — terrible UX for screen readers -->

for/id アプローチは通常 DOM で label と input を分離するため、スタイルを当てやすく、一般的に好まれます。placeholder を label の代わりに使わないでください — 認知障害のあるユーザーには機能せず、入力を始めた瞬間に消えてしまいます。W3C WAI label チュートリアルWebAIM フォームコントロールガイダンスにはアクセシビリティの根拠がより詳しく解説されています。

組み込みの制約バリデーション

HTML の組み込み制約バリデーションはフォーム送信前に実行され、本当に強力です。多くの便利な動作を無料で得られます:

html
<form>
  <!-- required: field must have a value -->
  <label for="fullname">Full name</label>
  <input type="text" id="fullname" name="fullname" required>

  <!-- minlength/maxlength: character count constraints -->
  <label for="username">Username (3–20 chars)</label>
  <input type="text" id="username" name="username"
         minlength="3" maxlength="20" required>

  <!-- pattern: regex-based validation -->
  <label for="postcode">UK Postcode</label>
  <input type="text" id="postcode" name="postcode"
         pattern="[A-Z]{1,2}[0-9][0-9A-Z]?s?[0-9][A-Z]{2}"
         title="Enter a valid UK postcode (e.g. SW1A 1AA)"
         required>

  <!-- min/max on date -->
  <label for="checkin">Check-in date</label>
  <input type="date" id="checkin" name="checkin"
         min="2026-04-16" required>

  <button type="submit">Submit</button>
</form>
  • requiredフィールドに値が必要です。チェックボックスの場合はチェックされている必要があります。
  • pattern値が一致しなければならない正規表現。必ず title 属性を追加しましょう — バリデーションのツールチップテキストになり、期待されるフォーマットのヒントをユーザーに提供します。
  • minlength / maxlengthテキスト input の文字数。maxlength は入力を黙って切り捨てます;minlength は送信時のみ検証します。
  • min / max数値または日付の範囲。numberdaterangetime の各 input に機能します。
  • step有効な増分を定義します。通貨フィールドの step="0.01" で銭単位が使えます。step="any" は step チェックを完全に無効にします。

グループ化のための fieldset と legend

関連する input のグループ — 特にラジオボタンやチェックボックス — がある場合、<fieldset><legend> でセマンティックにグループ化します。スクリーンリーダーはグループ内の各 input の前に legend を読み上げるため、ユーザーは常にラジオボタンがどの質問に答えるものかを把握できます。

html
<form>
  <fieldset>
    <legend>Preferred contact method</legend>

    <label>
      <input type="radio" name="contact" value="email" checked>
      Email
    </label>
    <label>
      <input type="radio" name="contact" value="phone">
      Phone
    </label>
    <label>
      <input type="radio" name="contact" value="post">
      Post
    </label>
  </fieldset>

  <fieldset>
    <legend>Notification preferences</legend>

    <label>
      <input type="checkbox" name="notify_new_posts" value="1">
      New articles
    </label>
    <label>
      <input type="checkbox" name="notify_replies" value="1">
      Replies to my comments
    </label>
  </fieldset>
</form>

制約バリデーション API

制約バリデーション API を使うと、ブラウザがネイティブに使用するのと同じバリデーションロジックに JavaScript からアクセスできます。これにより、送信をブロックせずにプログラムでバリデーションを実行し、カスタムエラーメッセージを設定できます:

js
const usernameInput = document.getElementById('username');

// Check if a single field is valid
console.log(usernameInput.checkValidity()); // true or false
console.log(usernameInput.validity.tooShort); // true if below minlength
console.log(usernameInput.validity.patternMismatch); // true if pattern fails

// Set a custom error message (shows in the browser's native validation tooltip)
usernameInput.setCustomValidity('That username is already taken.');
usernameInput.reportValidity(); // triggers the tooltip immediately

// Clear a custom error (important — once set, it sticks until cleared)
usernameInput.setCustomValidity('');

// Validate on the fly as the user types (async example — username availability)
usernameInput.addEventListener('input', async () => {
  const value = usernameInput.value;
  if (value.length < 3) return; // let minlength handle this

  const response = await fetch(`/api/check-username?q=${encodeURIComponent(value)}`);
  const { available } = await response.json();

  usernameInput.setCustomValidity(available ? '' : 'Username is already taken.');
});

重要な細部に注目してください:空でない文字列で setCustomValidity() を呼び出すと、setCustomValidity('') を呼び出してクリアするまでフィールドは永続的に無効になります。このクリア手順を忘れることが、この API を使う際の最も一般的なバグです。

カスタム JS バリデーション用の novalidate

スタイル付きエラーメッセージ(ブラウザのネイティブツールチップではない)を使ったカスタムバリデーション UI を構築する場合、フォームに novalidate を追加します。これによりブラウザのネイティブバリデーションは無効になりますが、独自のチェックのために制約バリデーション API は引き続き利用できます:

html
<form id="signup-form" novalidate>
  <div class="field">
    <label for="signup-email">Email</label>
    <input type="email" id="signup-email" name="email" required>
    <span class="error" aria-live="polite"></span>
  </div>

  <div class="field">
    <label for="signup-password">Password</label>
    <input type="password" id="signup-password" name="password" minlength="8" required>
    <span class="error" aria-live="polite"></span>
  </div>

  <button type="submit">Create account</button>
</form>
js
const form = document.getElementById('signup-form');

form.addEventListener('submit', (e) => {
  e.preventDefault();

  let isValid = true;

  form.querySelectorAll('input').forEach((input) => {
    const errorEl = input.nextElementSibling;

    if (!input.checkValidity()) {
      isValid = false;
      errorEl.textContent = input.validationMessage;
      input.setAttribute('aria-invalid', 'true');
    } else {
      errorEl.textContent = '';
      input.removeAttribute('aria-invalid');
    }
  });

  if (isValid) {
    form.submit();
  }
});

autocomplete と aria-describedby

フォームの UX を大幅に改善するが見落とされがちな2つの属性:

html
<!-- autocomplete: tells browsers/password managers what the field is for -->
<input type="text"     name="fname"    autocomplete="given-name">
<input type="text"     name="lname"    autocomplete="family-name">
<input type="email"    name="email"    autocomplete="email">
<input type="tel"      name="phone"    autocomplete="tel">
<input type="password" name="password" autocomplete="current-password">
<input type="password" name="new_pass" autocomplete="new-password">
<input type="text"     name="cc"       autocomplete="cc-number">

<!-- aria-describedby: links an input to a hint or error message -->
<label for="pass">Password</label>
<input
  type="password"
  id="pass"
  name="password"
  minlength="8"
  aria-describedby="pass-hint"
  required
>
<span id="pass-hint">Must be at least 8 characters.</span>

autocomplete 属性は WHATWG 仕様で定義された標準化されたトークン値を使用します。正しいトークンを使うと、ブラウザとパスワードマネージャーがフィールドを確実に自動入力できます。aria-describedby はヒントやエラーメッセージを input に関連付けます — スクリーンリーダーはフィールドラベルの後に説明を読み上げるため、ユーザーが入力を始める前に制約が聞こえるようになります。

完全なアクセシブルログインフォーム

すべてを組み合わせたものがこちらです — HTML と少量の JavaScript だけで、キーボードユーザー、スクリーンリーダーユーザー、モバイルユーザー全員に機能するログインフォームです:

html
<form id="login-form" novalidate>
  <h2>Sign in</h2>

  <div class="field">
    <label for="login-email">Email address</label>
    <input
      type="email"
      id="login-email"
      name="email"
      autocomplete="email"
      aria-describedby="email-error"
      required
    >
    <span id="email-error" class="error" aria-live="polite" role="alert"></span>
  </div>

  <div class="field">
    <label for="login-password">Password</label>
    <input
      type="password"
      id="login-password"
      name="password"
      autocomplete="current-password"
      aria-describedby="password-error"
      required
    >
    <span id="password-error" class="error" aria-live="polite" role="alert"></span>
    <a href="/forgot-password">Forgot password?</a>
  </div>

  <label class="inline">
    <input type="checkbox" name="remember" value="1">
    Keep me signed in for 30 days
  </label>

  <button type="submit">Sign in</button>
</form>

重要なポイント:エラー span の aria-live="polite" は、スクリーンリーダーがユーザーが現在聞いていることを中断せずにエラーが表示されたときに通知することを意味します。role="alert" は古いスクリーンリーダーのためにこれを補強します。autocomplete の値はパスワードマネージャーが期待するものと一致しているため、自動入力が最初から正常に機能します。

役立つツール

HTML フォームを構築してテストする際、HTML バリデーターはラベルの欠落や ID の重複などの構造的なミスをユーザーに届く前に検出します。一般的な HTML のクリーンアップには、HTML フォーマッターでマークアップを読みやすく保てます。MDN の input 要素リファレンスはすべての input タイプとその属性に関する最も完全なドキュメントです。

まとめ

HTML フォームにはほとんどのプロジェクトが活用している以上の機能が組み込まれています。正しい input タイプを選ぶだけで、モバイルキーボード、バリデーション、セマンティクスが無料で手に入ります。適切な label の関連付けと aria-describedby でマウスなしでフォームを操作できるようになります。制約バリデーション API はブラウザと戦うことなくネイティブバリデーションへの JavaScript フックを提供します。これらのピースを組み合わせると、カスタムバリデーションロジックを一行も書く前に、すべての人に機能するフォームができあがります。