htmx 逆引きレシピ
その場で入力エラーを出すには?(バリデーション)

公開日:
最終更新日:

「送信してからエラーが出る」ではなく、入力しているその場でエラーや注意を返せると、フォームの迷いが減って入力体験が一気に良くなります。
htmxなら、入力イベントに合わせてサーバーへ軽く問い合わせ、結果エリアだけを差し替えるだけで“即フィードバック”を実装できます。

このページでは、社員番号の重複チェックメール形式在庫不足を即警告の3例で、業務フォームで使える「その場バリデーション」パターンをデモ付きで解説します。
使うのは hx-post / hx-trigger / hx-target / hx-validate / hx-sync の基本だけです。

使用するhtmx属性

  • hx-post:入力値をPOSTで送って、検証結果のHTMLを返して差し替える(即バリデーション向き)
  • hx-trigger:検証を走らせるタイミングを指定(例:keyup changed delay:300ms / blur / change
  • hx-target:返ってきたHTMLを差し替える先の要素(エラーメッセージ表示エリア)を指定
  • hx-validate:リクエスト前にHTML5バリデーションを実行し、無効なら送信を止める(例:必須・形式チェックの下支え)
  • hx-sync:リクエスト競合を制御し、古い結果で上書きされる事故を防ぐ(例:this:replace

利用シーン

  • 「社員番号の重複チェック」:入力した社員番号が既に使われていないか、その場で確認したい
  • 「メール形式」:メールの形式・使用済みなどを、入力中に即フィードバックしたい
  • 「在庫不足を即警告」:商品と数量の入力に合わせて、在庫不足ならすぐ警告を出したい

① 社員番号の重複チェック

社員番号を入力すると、その場で「数字か」「桁数は足りるか」「重複していないか」をチェックする例です。
入力中に結果が出るので、送信後の差し戻しを減らしてスムーズに登録できます。

HTML

<div class="DEMO">

	<h4>① 社員番号の重複チェック</h4>

	<label>
		社員番号
		<input
			id="DEMO_EMP_NO"
			type="text"
			name="empno"
			maxlength="4"
			inputmode="numeric"
			autocomplete="off"
			placeholder="例:1001"
			hx-post="/htmx/demo/_validate_1.php"
			hx-trigger="keyup changed delay:300ms, blur"
			hx-target="#DEMO_EMP_NO_RESULT"
			hx-validate="true"
			hx-sync="this:replace"
		>
	</label>

	<div id="DEMO_EMP_NO_RESULT" class="FORM-RESULT" aria-live="polite">
		<span class="HTMX-NOTE">社員番号を入力すると、重複チェックします。</span>
	</div>

</div>

PHP

<?php
// 型を厳密に扱う(意図しない型変換を防ぐ)
declare(strict_types=1);

// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');

// POSTパラメータ empno(社員番号)を受け取る(なければ空)
$empno = (string)($_POST['empno'] ?? '');

// 前後空白を除去する
$empno = trim($empno);

// (デモ)少し待って“チェックしてる感”を出す
usleep(120000);

// 未入力なら案内を出して終了する
if ($empno === '') {
	// 入力案内を表示する
	echo '<span class="HTMX-NOTE">社員番号を入力すると、重複チェックします。</span>';
	// ここで終了する
	exit;
}

// 数字以外が混ざっていたらエラーを出して終了する
if (!preg_match('/^\d+$/', $empno)) {
	// エラー表示(数字のみ)
	echo '<div class="FORM-RESULT is-ng"><strong>数字のみ</strong>で入力してください。</div>';
	// ここで終了する
	exit;
}

// 桁数が短すぎる場合はエラーを出して終了する(例:4桁以上)
if (strlen($empno) < 4) {
	// エラー表示(桁数)
	echo '<div class="FORM-RESULT is-ng"><strong>4桁以上</strong>で入力してください。</div>';
	// ここで終了する
	exit;
}

// 既に存在する社員番号(デモ用。本番はDB検索に置き換え)
$exists = [
	'1001',
	'1002',
	'1024',
	'1100',
];

// 重複しているかどうかを判定する
$isDuplicate = in_array($empno, $exists, true);

// 重複している場合はエラーを返す
if ($isDuplicate) {
	// 重複エラーを表示する
	echo '<div class="FORM-RESULT is-ng"><strong>重複</strong>しています。別の番号を入力してください。</div>';
	// ここで終了する
	exit;
}

// 問題なければOKを返す
echo '<div class="FORM-RESULT is-ok"><strong>OK</strong>:使用できます。</div>';

デモ

① 社員番号の重複チェック

社員番号を入力すると、重複チェックします。

解説

  • 入力欄にhx-postを付け、社員番号(empno)をサーバーへ送って検証します。
  • hx-triggerkeyupdelayblur を拾い、入力中と入力後の両方でチェックします。
  • 検証結果(OK/NGメッセージ)はhx-targetで指定した結果エリアへ差し替え表示します。
  • hx-validate="true"により、HTML5の制約がある場合は無効な送信を抑止できます。
  • hx-sync="this:replace"で、連続入力でも古い結果が後から来て上書きされる事故を防ぎます。
  • PHP側は「未入力」「数字のみ」「桁数」「既存と一致(重複)」を順に判定し、短いHTML断片を返します。

② メール形式

メールアドレスを入力すると、その場で「メール形式か」「既に使用済みか」をチェックする例です。
入力直後にNG理由が分かるので、登録フォームの失敗を減らせます。

HTML

<div class="DEMO">

	<h4>② メール形式</h4>

	<label>
		メールアドレス
		<input
			id="DEMO_EMAIL"
			type="text"
			name="email"
			maxlength="64"
			autocomplete="off"
			placeholder="例:user@example.com"
			hx-post="/htmx/demo/_validate_2.php"
			hx-trigger="keyup changed delay:300ms, blur"
			hx-target="#DEMO_EMAIL_RESULT"
			hx-validate="true"
			hx-sync="this:replace"
		>
	</label>

	<div id="DEMO_EMAIL_RESULT" class="FORM-RESULT" aria-live="polite">
		<span class="HTMX-NOTE">メールアドレスを入力すると、その場で形式チェックします。</span>
	</div>

PHP

<?php
// 型を厳密に扱う(意図しない型変換を防ぐ)
declare(strict_types=1);

// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');

// POSTパラメータ email を受け取る(なければ空)
$email = (string)($_POST['email'] ?? '');

// 前後空白を除去する
$email = trim($email);

// (デモ)少し待って“チェックしてる感”を出す
usleep(120000);

// 未入力なら案内を出して終了する
if ($email === '') {
	// 入力案内を表示する
	echo '<span class="HTMX-NOTE">メールアドレスを入力すると、その場で形式チェックします。</span>';
	// ここで終了する
	exit;
}

// PHPの組み込みでメール形式を判定する
$isValid = (filter_var($email, FILTER_VALIDATE_EMAIL) !== false);

// 形式が不正ならエラーを返す
if (!$isValid) {
	// 形式エラーを表示する
	echo '<div class="FORM-RESULT is-ng"><strong>メール形式</strong>が正しくありません。</div>';
	// ここで終了する
	exit;
}

// (デモ)既に使われているメール(本番はDB検索に置き換え)
$used = [
	// 既存メール
	'test@example.com',
	// 既存メール
	'admin@example.com',
];

// 既存メールと一致するかを判定する(大文字小文字は同一扱いにする)
$isUsed = in_array(mb_strtolower($email), array_map('mb_strtolower', $used), true);

// 既に使われていたらエラーを返す
if ($isUsed) {
	// 使用済みエラーを表示する
	echo '<div class="FORM-RESULT is-ng"><strong>既に使用</strong>されています。</div>';
	// ここで終了する
	exit;
}

// 問題なければOKを返す
echo '<div class="FORM-RESULT is-ok"><strong>OK</strong>:使用できます。</div>';

デモ

② メール形式

メールアドレスを入力すると、その場で形式チェックします。

解説

  • 入力欄にhx-postを付け、メール(email)をサーバーへ送って検証します。
  • hx-triggerkeyup changed delayblur の組み合わせで、操作感と負荷のバランスを取ります。
  • 返ってきた検証結果HTMLを、hx-targetで指定した結果エリアへ差し替えます。
  • hx-validate="true"で、requiredtype="email"等のHTML5の制約がある場合は送信前に弾けます。
  • hx-sync="this:replace"で、入力の途中結果がズレて表示されるのを防止します。
  • PHP側は filter_var で形式判定し、(デモでは)使用済みリストと照合してOK/NGを返します。

③ 在庫不足を即警告

商品と数量を選ぶたびに在庫を確認し、足りなければその場で警告を出す例です。
「注文してから在庫不足」になりにくく、業務UIの入力ミスを早期に潰せます。

HTML

<div class="DEMO">

	<h4>③ 在庫不足を即警告</h4>

	<form
		id="DEMO_STOCK_FORM"
		class="FORM"
		hx-post="/htmx/demo/_validate_3.php"
		hx-trigger="change, keyup changed delay:300ms"
		hx-target="#DEMO_STOCK_RESULT"
		hx-validate="true"
		hx-sync="this:replace"
	>
		<label>
			商品
			<select name="sku">
				<option value="">選択してください</option>
				<option value="JUICE">みかんジュース</option>
				<option value="JELLY">みかんゼリー</option>
				<option value="BOX">みかん箱</option>
			</select>
		</label>

		<label>
			数量
			<input type="text" name="qty" maxlength="2" inputmode="numeric" placeholder="例:2">
		</label>
	</form>

	<div id="DEMO_STOCK_RESULT" class="FORM-RESULT" aria-live="polite">
		<span class="HTMX-NOTE">商品と数量を入力すると、在庫を確認して不足なら即警告します。</span>
	</div>

</div>

PHP

<?php
// 型を厳密に扱う(意図しない型変換を防ぐ)
declare(strict_types=1);

// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');

// POSTパラメータ sku(商品コード)を受け取る(なければ空)
$sku = (string)($_POST['sku'] ?? '');

// 前後空白を除去する
$sku = trim($sku);

// POSTパラメータ qty(数量)を受け取る(なければ空)
$qtyRaw = (string)($_POST['qty'] ?? '');

// 前後空白を除去する
$qtyRaw = trim($qtyRaw);

// (デモ)少し待って“チェックしてる感”を出す
usleep(120000);

// 在庫マスター(デモ用。本番はDB/APIに置き換え)
$stockMap = [
	// 商品コード => [商品名, 在庫数]
	'JUICE' => ['name' => 'みかんジュース', 'stock' => 5],
	// 商品コード => [商品名, 在庫数]
	'JELLY' => ['name' => 'みかんゼリー', 'stock' => 2],
	// 商品コード => [商品名, 在庫数]
	'BOX'   => ['name' => 'みかん箱', 'stock' => 0],
];

// 商品未選択なら案内を返して終了する
if ($sku === '') {
	// 案内を表示する
	echo '<span class="HTMX-NOTE">商品を選択してください。</span>';
	// ここで終了する
	exit;
}

// 商品コードが未知ならエラーを返して終了する
if (!array_key_exists($sku, $stockMap)) {
	// エラーを表示する
	echo '<div class="FORM-RESULT is-ng"><strong>不正な商品</strong>です。</div>';
	// ここで終了する
	exit;
}

// 数量が未入力なら案内を返して終了する
if ($qtyRaw === '') {
	// 案内を表示する
	echo '<span class="HTMX-NOTE">数量を入力してください。</span>';
	// ここで終了する
	exit;
}

// 数量が数字以外ならエラーを返して終了する
if (!preg_match('/^\d+$/', $qtyRaw)) {
	// エラーを表示する
	echo '<div class="FORM-RESULT is-ng"><strong>数量</strong>は数字のみで入力してください。</div>';
	// ここで終了する
	exit;
}

// 数量を整数に変換する
$qty = (int)$qtyRaw;

// 数量が0以下ならエラーを返して終了する
if ($qty <= 0) {
	// エラーを表示する
	echo '<div class="FORM-RESULT is-ng"><strong>数量</strong>は1以上で入力してください。</div>';
	// ここで終了する
	exit;
}

// 商品名を取り出す
$name = (string)$stockMap[$sku]['name'];

// 在庫数を取り出す
$stock = (int)$stockMap[$sku]['stock'];

// 在庫不足なら警告を返す
if ($qty > $stock) {
	// 不足数を計算する
	$lack = $qty - $stock;
	// 在庫不足メッセージを表示する
	echo '<div class="FORM-RESULT is-ng"><strong>在庫不足</strong>:' . $name . '(在庫 ' . $stock . ')/不足 ' . $lack . '</div>';
	// ここで終了する
	exit;
}

// ちょうど在庫ぴったりなら注意メッセージを返す
if ($qty === $stock) {
	// ぴったりメッセージを表示する
	echo '<div class="FORM-RESULT is-ok"><strong>OK</strong>:' . $name . '(在庫ちょうど ' . $stock . ')</div>';
	// ここで終了する
	exit;
}

// 在庫に余裕があればOKを返す
echo '<div class="FORM-RESULT is-ok"><strong>OK</strong>:' . $name . '(在庫 ' . $stock . ')</div>';

デモ

③ 在庫不足を即警告

商品と数量を入力すると、在庫を確認して不足なら即警告します。

解説

  • フォーム全体にhx-postを付け、商品(sku)と数量(qty)をまとめて送ります。
  • hx-triggerchange(商品変更)と keyup changed delay(数量入力)を拾い、条件が変わるたびに再チェックします。
  • 検証結果(在庫OK/不足警告)はhx-targetで指定した結果エリアへ差し替え表示します。
  • hx-validate="true"で、HTML側に制約がある場合は無効な送信を抑止できます。
  • hx-sync="this:replace"で、短時間に条件が変わっても最新結果を表示します。
  • PHP側は「未選択」「数量の妥当性」「在庫数との比較」を順に判定し、不足なら不足数まで含めた警告HTMLを返します。

エラーまとめ(よくある詰まりどころ)

  • 入力中に結果がチラつく/頻繁に通信してしまう
    対策:hx-triggerdelay:300ms を入れて間引く(例:keyup changed delay:300ms)。
  • 古いチェック結果が後から返ってきて、最新の状態を上書きする
    対策:hx-sync="this:replace" を付けて、同じ要素からの重なったリクエストは「新しいものを優先」する。
  • 未入力のときもエラーが出続けて、入力しづらい
    対策:未入力は「案内(NOTE)」を返す/blur時だけ厳しめ判定、入力中は軽め判定など、段階を分ける。
  • フォーム全体(商品+数量など)で検証したいのに、片方しか送られていない
    対策:フォームに hx-post を付けてまとめて送る(今回の在庫チェック方式)。入力単体なら<input>に付ける。
  • サーバー側で「未選択/未入力」の扱いがブレて、表示が不安定になる
    対策:PHPは最初に「未入力なら案内を返して終了」を統一する(ガード節を先頭に置く)。
  • HTML5バリデーションで送信が止まって、サーバー検証が走らない
    対策:hx-validate="true" を使う場合は、HTML側の required/pattern などの制約と役割を整理する(厳しすぎる制約は外す)。
  • 結果エリアが入れ替わらない/別の場所に入ってしまう
    対策:hx-targetのCSSセレクタを確認(IDの重複、スペルミス、要素の存在タイミング)。
  • PHPがHTMLを返しているのに表示が崩れる
    対策:レスポンスは「差し替えたい部分だけ」を返す(余計なHTML/改行/警告表示を出さない)。必要なら先頭で header('Content-Type: text/html; charset=UTF-8'); を統一する。
  • 日本語や記号を含む入力で検証が怪しい/比較がズレる
    対策:trim+正規化(必要なら)+比較は同一ルールで(メールは小文字化して照合など)。

このページの著者

もちもちみかん(システムエンジニア)

社内SEとしてグループ企業向けの業務アプリを要件定義〜運用まで一気通貫で担当しています。

経験:Webアプリ/業務システム

得意:PHP・JavaScript・MySQL・CSS

個人実績:フォーム生成基盤クイズ学習プラットフォーム

詳しいプロフィールはこちら!  もちもちみかんのプロフィール

もちもちみかん0系くん
TOPへ

もちもちみかん.comとは


このサイトでは、コーディングがめんどうくさい人向けのお助けツールとして、フォームやCSSをノーコードで生成できる、
 もちもちみかん.forms
 もちもちみかん.css1
 もちもちみかん.css2
と言ったジェネレーターを用意してます。

また、このサイトを通じて、「もちもちみかん」のかわいさを普及したいとかんがえてます!