ウェブアプリケーションの開発において、年齢・購入個数・順番など「正の整数のみ」をユーザーに入力させたい場面は非常に多く存在します。
しかし、安易に <input type="number"> を使うと、科学的表記を表す記号「e」や、マイナス符号(-)、プラス符号(+)、小数点(.)がキーボードやコピペによって容易に入力されてしまい、バリデーション漏れやデータ破壊の原因となります。
この記事では、なぜ HTML5 の number 型が「整数の入力制限」に向いていないのかを仕様レベルで紐解き、モバイル端末での入力性(UX)とアクセシビリティ(A11y)を最大化しつつ、完璧に整数だけを制限する実務的なベストプラクティスを解説します。
1. HTML5 <input type="number"> が抱える4つの問題点#
HTML5で導入された type="number" 属性は、一見すると数値の入力を強制する魔法の属性に見えますが、実際には以下の深刻な仕様上の欠陥があります。
問題①:科学的表記「e」の入力を防げない#
1.2e10 などの指数表記(浮動小数点数)は数学的に「正しい数値」とみなされるため、キーボードの e キーによる入力が標準で許可されてしまいます。
問題②:小数点と負符号が入力できてしまう#
整数のみに限定したい場合でも、. や - の入力をブラウザレベルで完全に無効化することはできません。
問題③:ブラウザによって挙動や値のパース方法が異なる#
ユーザーが 1.2.3 のような不正な形式の文字列を入力した場合、一部のブラウザでは内部的に ""(空文字)としてパースされるため、Reactの onChange イベントで「本当に空欄なのか、それとも不正な入力があったのか」を区別できなくなります。
問題④:step="1" の制限は不完全#
step="1" を指定すると、上下の矢印ボタン(スピンボタン)による増減こそ整数に制限されますが、直接のキーボードタイピングやコピー&ペーストによる小数の入力を防ぐ効果はありません。
2. 実務における最適解:type="text" + inputMode="numeric" + 正規表現フィルタ#
これらの問題をすべて解消し、かつスマートフォンの数字キーボード表示を損なわない業界標準の手法が、「あえて text 型を用い、状態変更時に厳密な正規表現フィルタリングをかける」 というアプローチです。
最適化された React コンポーネントの実装#
import React, { useState } from 'react';
export const IntegerInput: React.FC = () => {
const [value, setValue] = useState<string>("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const rawValue = e.target.value;
// 「空文字」または「0から9の数字のみ(整数)」にマッチする正規表現
if (rawValue === "" || /^[0-9]+$/.test(rawValue)) {
setValue(rawValue);
}
};
return (
<div className="flex flex-col gap-2">
<label htmlFor="integer-field" className="text-sm font-medium text-gray-700">
年齢入力(整数のみ)
</label>
<input
id="integer-field"
type="text"
inputMode="numeric" // Android等のモバイルブラウザで数字キーボードを開く
pattern="[0-9]*" // iOS Safariで数字テンキーを確実に開く
value={value}
onChange={handleChange}
placeholder="例: 25"
className="border rounded px-3 py-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
);
};このアプローチの圧倒的なメリット#
- 完璧な入力ガード: 記号、アルファベット、小数点は入力しようとした瞬間に React の State 更新が拒否されるため、UI上にも一切表示されません。
- 抜群のUX: モバイル端末では自動的に「数字専用のテンキーボード」が立ち上がるため、ユーザーは記号の存在しない非常にクリーンな操作感を得られます。
3. Zod を使った「型安全」なバリデーションと数値変換#
入力値を最終的にサーバーに送信する際、あるいは React Hook Form 等のフォームバリデーションで利用する際は、Zod スキーマを活用して「文字列から数値(number)型」へと安全に変換します。
z.coerce.number() を使って暗黙的に変換すると、小数が渡された場合に自動で丸め込まれるなどの予期せぬ挙動が発生するため、一度文字列として正規表現チェックを通した後に transform をかける手法が最も堅牢です。
堅牢な Zod スキーマ設計例#
import { z } from "zod";
const FormSchema = z.object({
// あえて string で入力値を受け取り、正規表現チェックをかけてから number に変換する
age: z
.string()
.min(1, "年齢は必須項目です")
.regex(/^[0-9]+$/, "半角整数のみで入力してください")
.transform((val) => {
// 安全に整数値へ変換
const parsed = parseInt(val, 10);
if (isNaN(parsed)) return 0;
return parsed;
})
.refine((num) => num >= 0 && num <= 120, {
message: "0歳から120歳の間で入力してください",
}),
});
type FormInputType = z.input<typeof FormSchema>; // 入力側の型(string)
type FormDataType = z.output<typeof FormSchema>; // 変換後の型(number)
4. アクセシビリティ(A11y)への配慮#
type="number" の代わりに type="text" を採用する場合、視覚障害者などが利用するスクリーンリーダーなどの支援技術に対して、そこが「数値を入力すべきフィールド」であることを明示する必要があります。
以下の実装を組み合わせることで、完全なアクセシビリティ仕様に適合させることができます。
<input
id="accessible-integer"
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={value}
onChange={handleChange}
// A11y属性の強化
role="spinbutton" // 数値入力用のスピンボタンUIと同等であることをスクリーンリーダーに通知
aria-valuenow={value ? parseInt(value, 10) : undefined}
aria-valuemin={0}
aria-valuemax={100}
aria-required="true"
/>5. 用途別の数値入力の最適実装マトリクス#
| 要件 | 推奨される実装 | キーポイント |
|---|---|---|
| 厳密な整数 (個数、年齢、並び順) | type="text" + inputMode="numeric" + 正規表現制御 | 小数点や記号を完全に防御し、モバイルではテンキーを表示 |
| 金額入力 (3桁ごとのカンマ区切り) | type="text" + フォーマットライブラリ(React Number Format等) | 1,000,000 のようなカンマ表示と内部数値の紐付け |
| 小数を許容する数値 (体重、緯度、経度) | type="number" + step="any" | 小数点をネイティブキーボードから入力できるようにする |
6. よくある質問(FAQ)#
Q. inputMode="numeric" と pattern="[0-9]*" の違いは何ですか?#
A. inputMode="numeric" は現在のHTML標準で定義されている、どのような入力キーボード(数値用か文字用か)を表示するかを指示する属性です。しかし、古い iOS Safari など一部の環境ではこれだけでは完璧に数字テンキーが開かないバグがあるため、歴史的なフォールバックとして pattern="[0-9]*" を併記することで、iOS環境においても確実にテンキーボードを立ち上げることができます。
Q. コピペ(貼り付け)によって小数や文字が入力された場合も防げますか?#
A. はい、防げます。今回の React 実装例では、キー入力だけでなく「ペーストなどによって発生する State の変化(onChange)」全体に対して正規表現マッチングをかけているため、アルファベットや小数を含んだ不正な文字列が貼り付けられた場合も、State更新そのものが拒否され、フォーム内には貼り付け前のデータが維持されます。
Q. <input type="number"> でマウススクロールした際に数値が勝手に変わってしまうのを防ぐには?#
A. これも type="number" の大きな問題点の1つです。type="number" を使うと、フォーカスが当たっている状態でマウスのホイールスクロールを行うと値が上下に暴走します。今回推奨している type="text" による手法であれば、このスクロール暴走バグも完全に回避することができます。











