# キーボードで打ち間違いを直せないワンタイムコードの入力欄

Source: https://easeweb.dev/learn/code-box-keys
Topics: フォーム > 分割されたコード入力欄に、キーボードで入力・修正できる (WCAG 2.1.1)
The fix: 打った数字にキーボードで戻れるようにします。コードを1つのテキスト欄で求めるか、欄を分けたままなら、keyup ではなく input イベントで次の欄へ移るようにします。できれば、Backspaceと矢印キーで欄の間を移れるようにし、貼り付けたコードを各欄に分けて入れます。
Test with: keyboard, screen reader, mouse
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete https://design-system.service.gov.uk/patterns/confirm-a-phone-number/

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

あるショップは、ログインに2段階目の確認を設けています。6桁のコードをテキストメッセージ（SMS）で送り、1桁ずつの6つの欄を表示します。1つの欄が埋まるとフォーカスがすぐ次の欄へ移るので、Tabキーを押さずに続けて打てます。「Larkspurの確認コード：481902」というメッセージが届きます。4、8と打ち、間違えて7を打ちます。フォーカスはもう4つ目の欄にあります。

Backspaceを押しても何も起きません。4つ目の欄は空なので、消すものがないからです。Shift+Tabで戻ろうとすると、フォーカスは3つ目の欄に着いた途端、4つ目の欄へ戻ります。左矢印キーも効きません。マウスなら、7をクリックして消し、1を打てば済みます。キーボードでは、どのキーでも7に届きません。残るのは、間違いと分かっているコードを送るか、ページを離れてログインをやり直すことだけです。

## 確認できる問題

達成基準2.1.1「キーボード」（レベルA）は、ページでできることをすべてキーボードでもできるよう求めています。このページでは、欄をクリックすればマウスで数字を直せます。それなら、キーボードでも直せなければなりません。

原因は、スクリプトが `keyup` を待っていることです。キーは押したときに働き、`keyup` のハンドラーは離したときに、その時点でフォーカスのある要素で動きます。Shift+Tabを押すと、フォーカスは3つ目の欄へ移ります。Tabを離すと、その `keyup` が3つ目の欄に届きます。欄には数字が入っているので、スクリプトはフォーカスを4つ目へ送ります。どのキーでも同じことが起きるため、数字の入った欄にとどまれるキーはありません。Backspaceと矢印キーは、フォーカスのある空の欄の中で働くだけで、前へ戻す処理はスクリプトにありません。Tabで先へ進めばこの欄から出られるので、キーボードトラップではありません。塞がれているのは、戻る道です。Chromium、Firefox、Safariのエンジンで、同じ動きを確認しました。

[貼り付けられないワンタイムコードの入力欄](/ja/learn/one-time-codes)のように、`input` イベントで次の欄へ移る欄は、Backspaceを扱っていなくても、このようにはなりません。`input` イベントは値が変わったときにだけ発生するため、Shift+Tabで3つ目の欄にとどまれます。ブラウザーはTabで着いた欄の数字を選択するので、1を打てば7と置き換わります。ほかの作り方の欄では、別の理由で同じことが起きます。`div` 要素で描き、ページ側のキーイベントのハンドラーで数字を入れる欄には、スクリプトで1つずつ加えない限り、入力欄が本来持つキー操作がありません。

## 利用者への影響

ログイン用のコードは分単位で期限が切れることが多く、何度か間違えるとアカウントがロックされることもあります。直せない1桁は、打ち直しの手間だけでは済みません。

## 自動チェックとAIの限界

どの欄にもラベルがあるため、axeなどの自動チェックは合格にします。スクリプトがいつフォーカスを移すかは、マークアップには表れません。EaseWebの診断は、`maxlength="1"` の欄が4つ以上並んでいると、最初の欄から2桁を打ってShift+Tabを押し、フォーカスがすぐ3つ目の欄へ戻されたら `ew:code-boxes-keyboard` として報告します。打つのは2桁だけで、コード全体は打ちません。自動で送信されることがあるためです。input 要素でない欄は調べません。`keyup` で値の長さを `maxlength` と比べて次の欄へ移す書き方は、掲示板の回答やプラグインで以前から使われています。1回で正しく打てば問題なく動くため、正しく打つだけのテストでは何も見つかりません。

## 修正前：キーを離すと次の欄へ移る欄

```html
<p id="code-label">Enter the 6-digit code we sent to your phone</p>
<div class="code-boxes" role="group" aria-labelledby="code-label">
  <input name="code-1" inputmode="numeric" maxlength="1" aria-label="Digit 1 of 6">
  <input name="code-2" inputmode="numeric" maxlength="1" aria-label="Digit 2 of 6">
  <input name="code-3" inputmode="numeric" maxlength="1" aria-label="Digit 3 of 6">
  <input name="code-4" inputmode="numeric" maxlength="1" aria-label="Digit 4 of 6">
  <input name="code-5" inputmode="numeric" maxlength="1" aria-label="Digit 5 of 6">
  <input name="code-6" inputmode="numeric" maxlength="1" aria-label="Digit 6 of 6">
</div>
<button type="submit">Continue</button>
```

```js
const boxes = [...document.querySelectorAll('.code-boxes input')];
// When a box is full, move on to the next one.
boxes.forEach((box, i) => box.addEventListener('keyup', () => {
  if (box.value.length === box.maxLength && boxes[i + 1]) boxes[i + 1].focus();
}));
```

ハンドラーは、打った数字だけでなく、離されたすべてのキーで動きます。Shift+Tabで数字の入った欄に着くと、Tabキーの `keyup` がそこに届き、フォーカスはまた先へ移ります。キーボードでは、直したい数字の欄にとどまれません。

## 修正後：コード全体を受け取る1つの欄

```html
<label for="code">Security code</label>
<p id="code-hint">Enter the 6-digit code we sent to your phone.</p>
<input id="code" name="code" type="text" inputmode="numeric"
  autocomplete="one-time-code" spellcheck="false" aria-describedby="code-hint">
<button type="submit">Continue</button>
```

```js
// No script: typing, Backspace, the arrow keys and paste are the browser's own.
```

1つのテキスト欄なら、編集に使うキーはどれも、ふつうの入力欄と同じように働きます。Backspaceで7が消え、矢印キーでカーソルが動き、Shift+Tabで前の要素へ戻ります。フォーカスを移すものがないので、間違った方向へ移されることもありません。貼り付けたコードも丸ごと入り、`autocomplete="one-time-code"` に対応するブラウザーは、新しいメッセージのコードを候補に出せます。

## 修正後：keyupではなくinputで次の欄へ移る

```html
<p id="code-label">Enter the 6-digit code we sent to your phone</p>
<div class="code-boxes" role="group" aria-labelledby="code-label">
  <input name="code-1" inputmode="numeric" maxlength="1" aria-label="Digit 1 of 6">
  <input name="code-2" inputmode="numeric" maxlength="1" aria-label="Digit 2 of 6">
  <input name="code-3" inputmode="numeric" maxlength="1" aria-label="Digit 3 of 6">
  <input name="code-4" inputmode="numeric" maxlength="1" aria-label="Digit 4 of 6">
  <input name="code-5" inputmode="numeric" maxlength="1" aria-label="Digit 5 of 6">
  <input name="code-6" inputmode="numeric" maxlength="1" aria-label="Digit 6 of 6">
</div>
<button type="submit">Continue</button>
```

```js
const boxes = [...document.querySelectorAll('.code-boxes input')];
// When a digit goes in, move on to the next box.
boxes.forEach((box, i) => box.addEventListener('input', () => {
  if (box.value && boxes[i + 1]) boxes[i + 1].focus();
}));
```

いちばん小さな変更で、変えるのはイベント名1つです。`input` イベントはキーを離したときではなく値が変わったときに発生するため、Tab、Shift、矢印キーではフォーカスが移らなくなります。Shift+Tabで3つ目の欄にとどまり、Tabで着いたときにブラウザーが7を選択するので、1を打てば置き換わって次の欄へ進みます。変わらないことが2つあります。空の4つ目の欄でBackspaceを押しても、利用者が期待するようには前へ戻りません。また各欄は1文字しか入らないため、貼り付けたコードは最初の1桁しか残らず、3.3.8を満たしません。詳しくは[貼り付けられないワンタイムコードの入力欄](/ja/learn/one-time-codes)で説明しています。

## 修正後：Backspaceと矢印キーで移れる欄

```html
<p id="code-label">Enter the 6-digit code we sent to your phone</p>
<div class="code-boxes" role="group" aria-labelledby="code-label">
  <input name="code-1" inputmode="numeric" autocomplete="one-time-code" aria-label="Digit 1 of 6">
  <input name="code-2" inputmode="numeric" aria-label="Digit 2 of 6">
  <input name="code-3" inputmode="numeric" aria-label="Digit 3 of 6">
  <input name="code-4" inputmode="numeric" aria-label="Digit 4 of 6">
  <input name="code-5" inputmode="numeric" aria-label="Digit 5 of 6">
  <input name="code-6" inputmode="numeric" aria-label="Digit 6 of 6">
</div>
<button type="submit">Continue</button>
```

```js
const boxes = [...document.querySelectorAll('.code-boxes input')];
boxes.forEach((box, i) => {
  // A typed digit fills this box and moves on; a pasted, autofilled
  // or dictated code is spread over this box and the ones after it.
  box.addEventListener('input', (event) => {
    const digits = (event.data ?? box.value).replace(/\D/g, '').slice(0, boxes.length - i);
    box.value = '';
    [...digits].forEach((digit, n) => { boxes[i + n].value = digit; });
    if (digits) boxes[Math.min(i + digits.length, boxes.length - 1)].focus();
  });
  // Arriving in a box selects its digit, ready to be typed over.
  box.addEventListener('focus', () => box.select());
  box.addEventListener('keydown', (event) => {
    // Backspace in an empty box deletes the digit before it.
    if (event.key === 'Backspace' && !box.value && i > 0) {
      event.preventDefault();
      boxes[i - 1].value = '';
      boxes[i - 1].focus();
    }
    // The arrow keys move between the boxes.
    if (event.key === 'ArrowLeft' && i > 0) {
      event.preventDefault();
      boxes[i - 1].focus();
    }
    if (event.key === 'ArrowRight' && boxes[i + 1]) {
      event.preventDefault();
      boxes[i + 1].focus();
    }
  });
});
```

1つの欄で使えるキーが、欄に分けても使えるようになります。空の4つ目の欄でBackspaceを押すと7が消えてその欄へ戻り、左矢印キーでは消さずに戻ります。Shift+Tabもそのまま使えます。キー操作で欄に着くと数字が選択され、スクリプトは打ったばかりの数字だけを残すので、クリックで着いた場合も、打てば置き換わります。次の欄へ移るのは `input` イベントのときだけです。`maxlength` がないため、貼り付けたコードは丸ごとスクリプトに届いて各欄に分けられ、3.3.8も満たします。これらのキー、クリック、貼り付けをChromiumで、キーとクリックをFirefoxとSafariのエンジンでも確認しました。

## どの方法を選ぶか

3つとも、キーボードで数字を直せるようになります。1つの欄にしてください。スクリプトなしで役目を果たし、編集のキーも貼り付けもブラウザーが扱うため、保守するものがありません。`keyup` を `input` に変えるのは、公開中の欄をいちばん早く直す方法ですが、貼り付けの問題と、前へ戻らないBackspaceが残ります。最初の一歩と考えてください。デザインを変えられず欄に分けたままにする場合は、最後の方法のように、すべてのキーを使えるようにし、各欄が次の欄へ自動で進むことを欄のそばに書きます（「実装の判断」を参照）。

## 実装の判断

次の欄へフォーカスを移すのは、値が変わったときにだけ発生する `input` イベントにします。どのキーにも反応する `keyup` や `keydown` のハンドラーでは移しません。Tab、Shift、矢印キーでも動いてしまうからです。「欄が埋まっているか」を確かめても防げません。数字の入った欄にShift+Tabで着いたときこそ、問題が起きる場面だからです。

ブラウザーは、TabやShift+Tabで着いた欄の中身を選択します。小さな修正で上から打ち直せるのはこのためです。クリックで着いた場合はカーソルが置かれるだけで、ChromiumとFirefoxではスクリプトの `focus()` でも同じです。`maxlength="1"` の欄では、数字の横に打った数字は拒まれます。最後の方法は、フォーカスが来たときに数字を選択し、`input` のハンドラーで打ったばかりの数字だけを残します。そのため、どう着いても打てば置き換わります。Safariのエンジンではクリックで選択が解除されますが、それでも置き換わります。

欄に分ける2つの修正は、今もフォーカスを自動で移します。フォーカスの移動は、3.2.2 入力時の「状況の変化」に当たります。Understanding文書には、自動で次の欄へ進む分割された電話番号の欄の例がありますが、基準を満たすのは、フォームの冒頭でその動きを説明しているからです。欄の上の説明に「入力すると、自動で次の欄に進みます」のように書いてください。1つの欄なら何も移らないので、これも1つの欄を選ぶ理由です。

コードは、ボタンかEnterキーで利用者が送るようにします。最後の欄が埋まった瞬間に送信するフォームは、頼まれずにページを変えることになり、[操作するだけでページが変わるドロップダウンと入力欄](/ja/learn/context-changes)で説明している問題に当たります。打ち間違えた数字は、直す間もなく1回の失敗として数えられてしまいます。

日本語の入力方式（IME）では、数字が全角で入ることがあります。`\D` は全角数字も取り除くため、0〜9だけを残す欄は、理由も示さずに空のままになることがあります。[全角数字を受け付けず、理由も示さない入力欄](/ja/learn/full-width-digits)で説明しているとおり、先に半角に変換してください。

## 修正の確認

1. マウスを使わずに、3桁目を間違えたコードを打ち、Shift+Tabを押して正しい数字を打つ。間違った数字と置き換わり、打つまでフォーカスが移った先にとどまることを確認する。
2. 最初の欄から「Continue」までTabで進み、Shift+Tabで最初の欄まで戻る。行きも帰りも、各欄に順に一度ずつ止まることを確認する。
3. Windowsのスクリーンキーボードや、macOSのアクセシビリティキーボードで、1桁間違えたコードを打って直す。
4. スクリーンリーダーで、コードを打ってからShift+Tabで戻る。各欄がラベルと中の数字とともに読まれ、フォーカスがひとりでに先へ移らないことを確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。ログインの流れやコードの部品を変更した際は、再度確認します。

## 制限

このサンプルは、どのコードの入力でも同じ結果になることを保証するものではありません。例はログイン用のコードです。PIN、ギフトカードの番号、予約番号などを欄に分けて求める場合も、同じように確かめてください。コード全体の貼り付けは、[貼り付けられないワンタイムコードの入力欄](/ja/learn/one-time-codes)で扱っています。input でない要素で作った欄は、すべてのキーをスクリプトで加える必要があり、スクリーンリーダーが入力欄として扱わないこともあります。このガイドでは、そのコードは扱いません。利用者が頼るキーボード、スイッチ、スクリーンリーダーで、ログイン全体を確認してください。

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
