入力欄 デザイン 9選 — コピペで動くフォーム部品
入力欄 デザインとは、電話番号やカード番号、数量、タグのように会員登録や購入フォームでよく使うのに、素のHTMLだけでは思うように動かない入力ウィジェットのことです。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 電話番号の自動ハイフン
- 02 カード番号4桁区切り
- 03 金額のカンマ自動入力
- 04 数量 +/- ステッパー
- 05 タグ入力欄
- 06 自動で伸びるtextarea
- 07 ファイルのドラッグ&ドロップアップロード
- 08 認証コード再送信タイマーボタン
- 09 リアルタイム入力検証表示
9個は人気順ではなく、フォームを実際に埋めていく順番で並べています。連絡先と決済情報の入力(01〜03)、数量とタグの選択(04〜05)、メモの記入と添付(06〜07)、最後に本人確認と送信前の検証(08〜09)という流れです。
| # | 部品 | いつ使うか |
|---|---|---|
| 01 | 電話番号の自動ハイフン | 会員登録・配送先連絡先 |
| 02 | カード番号4桁区切り | 決済フォームのカード欄 |
| 03 | 金額のカンマ自動入力 | 寄付・見積もり金額 |
| 04 | 数量 +/- ステッパー | カート数量・予約人数 |
| 05 | タグ入力欄 | キーワード・宛先の複数登録 |
| 06 | 自動で伸びるtextarea | レビュー・問い合わせ内容 |
| 07 | ファイルのドラッグ&ドロップ | プロフィール写真・添付 |
| 08 | 認証コード再送信タイマーボタン | 電話・メールの再認証 |
| 09 | リアルタイム入力検証表示 | メール・パスワードの検証 |
01電話番号の自動ハイフン
数字だけ入力しても、010-1234-5678のように自動的にハイフンが入ります。まとめて貼り付けても同じルールで整形されます。
function format(raw) {
const d = raw.replace(/\D/g, "").slice(0, 11);
if (d.length < 4) return d;
if (d.length < 8) return d.slice(0, 3) + "-" + d.slice(3);
return d.slice(0, 3) + "-" + d.slice(3, 7) + "-" + d.slice(7);
}
input.addEventListener("input", () => {
input.value = format(input.value);
});
02カード番号4桁区切り
カード番号を入力すると4桁ごとに自動で空白が入り、実際のカードのように見えます。先頭の数字からVISA・Mastercard・AMEXも判定します。
function format(raw) {
const d = raw.replace(/\D/g, "").slice(0, 16);
const groups = d.match(/.{1,4}/g) || [];
return groups.join(" ");
}
function brandOf(d) {
if (d[0] === "4") return "VISA";
if (/^5[1-5]/.test(d)) return "Mastercard";
return "";
}
03金額のカンマ自動入力
円マークが付いた入力欄に数字を入れると、3桁ごとに自動でカンマが付きます。先頭の余分な0も同時に取り除かれます。
function format(raw) {
const d = raw.replace(/\D/g, "").replace(/^0+(?=\d)/, "").slice(0, 12);
return d ? Number(d).toLocaleString("ja-JP") : "";
}
input.addEventListener("input", () => {
input.value = format(input.value);
});
04数量 +/- ステッパー
ボタンを押すたびに中央の数字が1ずつ変わり、最小値(1)になると−ボタンが自動的に薄くなって反応しなくなります。
const MIN = 1, MAX = 99;
function set(v) {
n = Math.min(MAX, Math.max(MIN, v));
value.value = n;
minus.disabled = n <= MIN;
plus.disabled = n >= MAX;
}
minus.addEventListener("click", () => set(n - 1));
plus.addEventListener("click", () => set(n + 1));
05タグ入力欄
文字を入力してEnterを押すとピル型のタグになり、空の入力欄でバックスペースを押すと最後のタグが消えます。
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
if (input.value.trim()) tags.push(input.value.trim());
input.value = "";
render();
} else if (e.key === "Backspace" && !input.value && tags.length) {
tags.pop();
render();
}
});
06自動で伸びるtextarea
行が増えるたびにスクロールの代わりに入力欄自体の高さが伸び、決めた最大の高さ(132px)を超えるとそこからスクロールになります。
function resize() {
// 一度autoに戻さないと、縮んだ分の高さが反映されない
area.style.height = "auto";
const next = Math.min(area.scrollHeight, 132);
area.style.height = next + "px";
}
area.addEventListener("input", resize);
07ファイルのドラッグ&ドロップ
ファイルをドラッグすると枠線が太くなり、ドロップした瞬間にファイル名がチップに変わります。クリックすれば通常のファイル選択も開きます。
zone.addEventListener("dragover", (e) => {
e.preventDefault();
zone.classList.add("is-over");
});
zone.addEventListener("drop", (e) => {
e.preventDefault();
zone.classList.remove("is-over");
const file = e.dataTransfer.files[0];
if (file) filename.textContent = "選択済み: " + file.name;
});
08認証コード再送信タイマーボタン
再送信ボタンを押すと60秒間無効になり、残り秒数がボタンの文字の上でリアルタイムに減っていき、0秒でまた押せるようになります。
function cooldown(seconds) {
let left = seconds;
btn.disabled = true;
btn.textContent = left + "秒後に再送信";
const t = setInterval(() => {
left--;
if (left <= 0) {
clearInterval(t);
btn.disabled = false;
btn.textContent = "再送信";
} else {
btn.textContent = left + "秒後に再送信";
}
}, 1000);
}
09リアルタイム入力検証表示
入力している間、メールの形式が合えば緑のチェックが、間違っていれば赤いメッセージがその場ですぐ表示されます。空欄のときは何も表示しません。
const RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
input.addEventListener("input", () => {
const v = input.value;
frame.classList.remove("is-ok", "is-bad");
if (!v) { icon.textContent = ""; msg.textContent = ""; return; }
if (RE.test(v)) { frame.classList.add("is-ok"); icon.textContent = "✓"; }
else { frame.classList.add("is-bad"); icon.textContent = "!"; msg.textContent = "メール形式をご確認ください"; }
});
どこで壊れるか — 落とし穴ひとつ
01・02・03の自動整形入力欄は、すべて同じ落とし穴を抱えています。input.value = format(value)のように値をまるごと書き直すと、ブラウザはカーソル位置を必ず文字列の末尾に戻してしまいます。途中の桁を直そうと入力欄の真ん中をクリックしても、次の一文字を打った瞬間にカーソルが末尾へ飛んでしまうのです。電話番号やカード番号のような短い入力ではさほど気になりませんが、長く編集する入力欄ではMDNのHTMLInputElement.setSelectionRangeのドキュメントを参考に、整形前にカーソル位置を保存して整形後に戻す処理が必要です。この落とし穴を含めて9個のファイルすべてに実際に動作するバニラJSとReact版を収めた圧縮ファイルのパスワードはpuc8udvvで、下のボタンから受け取れます。
07のドラッグ&ドロップにも似た見落としがあります。dragoverイベントでpreventDefault()を呼ばないと、ブラウザの既定の動作(ファイルを新しいタブで開く)が実行されてしまい、dropイベント自体が発生しません。上のコードにこの処理が入っているのはそのためです。
アクセシビリティ
9個すべてでラベルと入力欄をlabelとforで結び付けており、04の数量ステッパーは画面に見えないaria-live領域で「数量3個」のように変更値をスクリーンリーダーに知らせます。08の再送信ボタンはaria-live="polite"の状態文で送信完了を伝え、09の検証メッセージはrole="status"で値が変わったときだけ静かに更新されます。07のファイル入力は画面上では隠れていますがTabでフォーカスが移り、EnterかSpaceでファイル選択が開くため、マウスなしでも最後まで操作できます。色や枠線の切り替えはすべてMDNのprefers-reduced-motionのドキュメントに沿って処理しています。
@media (prefers-reduced-motion: reduce) {
.field__input, .drop__zone, .valid__frame { transition: none; }
}
ほかの入力フォーム関連の記事は認証コード 入力画面が直面する9つの状況で読めますし、このサイトが何を扱っているかは紹介ページにまとめています。
FAQ
この9個はReactのプロジェクトにもそのまま使えますか?
はい。圧縮ファイルのreact/フォルダには項目ごとに.tsxと.module.scssが入っており、vanilla/のバニラJSと同じ動作をフックに移し替えてあります。色やラベルの文字列をpropsで渡すだけで使えます。
カード番号の自動整形は決済のセキュリティに関係しますか?
いいえ。ここで扱っているのは数字を4桁ごとに読みやすく画面表示するだけの整形処理で、カード情報を実際に処理・保存するのは決済代行会社の役目です。この整形自体はカードのデータをサーバーへ送信しません。
電話番号やカード番号の入力欄では、スマートフォンで数字キーパッドが自動で開きますか?
はい。inputmode="numeric"を指定しているため、多くのモバイルブラウザは文字キーボードの代わりに数字キーパッドを先に表示します。ただしこれはあくまでヒントなので、ブラウザによって多少動作が異なることがあります。