# 貼り付けられないワンタイムコードの入力欄

Source: https://easeweb.dev/learn/one-time-codes
Topics: フォーム > ワンタイムコードを1つの入力欄に貼り付けられる (WCAG 3.3.8, 2.1.1)
The fix: コードを一度に全部入れられるようにします。autocomplete="one-time-code" を付け、貼り付けを止めない1つのテキスト欄で求めるか、欄はそのままで maxlength="1" を外し、貼り付け、自動入力、音声入力したコードをスクリプトで各欄に分けて入れます。
Test with: keyboard, screen reader, touch, autofill, speech input
References: https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-minimum.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F109 https://www.w3.org/WAI/WCAG22/Techniques/failures/F36 https://design-system.service.gov.uk/patterns/confirm-a-phone-number/ https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete

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

## 場面

あるショップは、ログインに2段階目の確認を設けています。パスワードの後、6桁のコードをテキストメッセージ（SMS）で送り、1桁ずつの6つの欄を持つページを表示します。「Larkspurの確認コード：481902」というメッセージが届きます。コードをコピーして最初の欄に貼り付けると、4だけが入ります。各欄は1文字しか受け付けないため、ブラウザーは貼り付けたコードを最初の1桁に切り詰め、残りの5つの欄は空のままです。新しいメッセージからスマートフォンが候補に出すコードを選んでも、同じように入ることがあります。残る方法は、メッセージのコードを読み、1桁ずつ別の画面に打ち直すことだけです。

同じ手順は、ほかの形でもつまずきます。

- 欄は貼り付けを受け付けるが、ちょうど6桁のときだけなので、一部のメッセージのように途中に空白があるコードをコピーすると何も起きない。
- 1つのコード欄に `maxlength="6"` があり、「481 902」とコピーしたコードが「481 90」に切られて受け付けられない。
- 1桁ずつ4つの欄で求めるPINを、パスワードマネージャーが入力できない。

## 確認できる問題

達成基準3.3.8「アクセシブルな認証（最低限）」はレベルAAで、WCAG 2.2で新しく加わりました。ログインのどの段階も、パスワードを覚えることやコードを書き写すことのような認知機能テストに頼ってはいけません。ただし、別のログイン方法があるか、パスワードマネージャーやコピーと貼り付けのような助けがあれば構いません。メッセージのコードを打ち込むことは書き写しです。達成基準の解説書は、確認コードを手で打ち込ませるサービスは適合しないとし、少なくともコードを貼り付けられる必要があるとしています。

欄に分けた入力は、失敗例F109に当たります。1文字ずつ欄に入れるコードは不適合です。ただし、コード全体を最初の欄に貼り付けられ、ほかの欄が自動で埋まる場合は除きます。貼り付けを止める欄も同じく不適合です。打ち込む以外の入れ方がなくなるからです。どちらも、メールで届くログイン用のリンクのように、書き写しのいらない別の方法を同じログインで選べるなら適合します。

この基準が対象にするのは、コードをページに入れることで、コードを本人に届けることではありません。ログインする端末にコードが届くか、別の端末からそこへコピーできるかは、この基準の対象外です。

## 利用者への影響

スマートフォンでは、メッセージとページが別のアプリにあります。コードを打つにはその間を何度も行き来することになり、その間もページではタイマーが進みます。

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

どの欄にもラベルがあるため、axeなどの自動チェックは合格にします。EaseWebの診断は、ページのどこも貼り付けを受け取らない1文字ずつの欄の並びを報告します。そこでは、ブラウザーがどんな貼り付けも1文字に切り詰めるからです。また、試しの貼り付けを取り消し、何も入れないコードの欄も報告します。欄が独自に貼り付けを扱っていて、試しの貼り付けが各欄に分かれない場合は、確認の依頼を出します。診断が貼り付けるのは2桁だけで、コード全体は貼り付けません。全桁が入ると自動で送信され、実際のアカウントで失敗した試行として数えられることがあるためです。実際のコードは人が試す必要があります。診断には、コードが届くか、スマートフォンが候補に出すかは分かりません。AIのコードアシスタントに「OTPの入力欄」を頼むと、`maxlength="1"` の6つの欄を作り、1桁打つごとに次の欄へ移るようにして、貼り付けはまったく扱わないことがよくあります。

## 修正前：1文字ずつの6つの欄

```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 inputmode="numeric" maxlength="1" aria-label="Digit 1">
  <input inputmode="numeric" maxlength="1" aria-label="Digit 2">
  <input inputmode="numeric" maxlength="1" aria-label="Digit 3">
  <input inputmode="numeric" maxlength="1" aria-label="Digit 4">
  <input inputmode="numeric" maxlength="1" aria-label="Digit 5">
  <input inputmode="numeric" maxlength="1" aria-label="Digit 6">
</div>
```

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

`maxlength="1"` があると、貼り付けたコードも、自動入力や音声入力のコードも、スクリプトの `input` ハンドラーが受け取る前にブラウザーが最初の1文字に切り詰めます。残りをほかの欄に分けるものがないため、コードは1桁ずつ打ち込むしかありません。

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

```html
<label for="code">Security code</label>
<p id="code-hint">We sent a 6-digit code to your phone. It works for 15 minutes.</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>
```

コードは、ほかの文字と同じように入ります。キーボードやコンテキストメニューからの貼り付けも、音声入力も、ブラウザーの自動入力も使えます。`autocomplete="one-time-code"` で欄の目的をブラウザーに伝えるため、Safariのように対応するブラウザーは、新しいメッセージのコードをキーボードの上に候補として出せます。`inputmode="numeric"` で、スマートフォンでは数字のキーパッドが出ます。`maxlength` はないため、「481 902」と貼り付けても入ります。サーバーは、空白とハイフンを除いてからコードを確かめます。どれにもスクリプトはいりません。

## 修正後：貼り付けたコードを各欄に分ける欄

```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 pasted, autofilled or dictated code arrives as several characters at once:
  // spread its digits over this box and the ones after it.
  box.addEventListener('input', () => {
    const digits = 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();
  });
  // Typing over a filled box replaces its digit.
  box.addEventListener('focus', () => box.select());
  // Backspace in an empty box goes back to the one before it.
  box.addEventListener('keydown', (event) => {
    if (event.key === 'Backspace' && !box.value && i > 0) boxes[i - 1].focus();
  });
});
```

欄に `maxlength` がないため、貼り付けたコードは丸ごとスクリプトに届き、スクリプトは入った欄から順に1桁ずつ入れていきます。`input` イベントで扱うため、同じコードで貼り付け、スマートフォンが候補に出すコード、音声入力のどれにも対応します。これはF109が挙げる例外の形です。コード全体を最初の欄に貼り付けられ、ほかの欄が埋まります。ただし、スクリプトが動き、部品ライブラリーが独自に持つ貼り付けの処理など、ページのほかのものが先にコードを切り詰めない間に限ります。サーバーは、6つの値を空白を除いてつなげてから、コードを確かめます。

## どの方法を選ぶか

どちらも3.3.8を満たします。1つの欄にしてください。スクリプトなしで同じ役目を果たし、貼り付け、スマートフォンが候補に出すコード、音声入力、パスワードマネージャーのすべてをブラウザーが扱うため、どれも動きます。欄に分けると、そのそれぞれを正しく扱うスクリプトが必要です。スクリーンリーダーでも、6つではなく1つの欄として一度で読まれます。欄に分けるのは、デザインを変えられない場合だけにし、その場合は上のように、複数の文字の入力をすべて各欄に分けてください。

## 実装の判断

コードに `type="number"` は使わないでください。コードは数量ではありません。ブラウザーはそこでは `maxlength` を無視し、矢印キーで値が増減し、ハイフンの入ったコードを貼り付けると数でないとして捨てられます。`inputmode="numeric"` を付けたテキスト欄を使い、英字が入るコードでは `inputmode` を省きます。

コードは、ボタンかEnterキーで利用者が送るようにします。最後の桁が入った瞬間に送信するフォームは、そうなることを前もってページで伝えていない限り、3.2.2の不適合（失敗例F36）になります。数字を1つ間違えると、確かめる前に失敗した試行になってしまいます。

コードには時間を与えてください。30秒で切れるコードは制限時間です。2.2.1の解説書は、セキュリティのために有効期限を設けたログイン用のコードは必要不可欠とみなされ、この基準の対象外になりうるとしていますが、ゆっくり打つ人や、スクリーンリーダーでアプリを行き来する人には、ずっと長い時間が必要です。GOV.UKは15分としています。「新しいコードを送る」ボタンを、キーボードで使える本物のボタンとして置くと、コードが切れた人や届かなかった人の助けになります。

コードを間隔を空けて見せたいデザインでは、欄を分けずに1つの欄のスタイルで表します。`letter-spacing` と `font-variant-numeric: tabular-nums` で、6つの欄なしで数字の間が空きます。

PINなど、1文字ずつの欄で求めるほかのものも同じです。パスワードマネージャーは1つの欄なら入力できても、6つの欄には入力できません。PINを覚えていられない人には、貼り付けや自動入力が必要です。メールで届くリンクやパスキーのように、書き写しのいらない別のログイン方法を用意することも3.3.8を満たしますが、コードの手順を使う人のために、その手順も貼り付けを受け付けるようにしてください。

## 修正の確認

1. テキストメッセージやメールの実際のコードをコピーし、Ctrl + VかCommand + Vとコンテキストメニューの両方で、欄または最初の欄に貼り付け、すべての桁が入るか確認する。
2. スマートフォンで、新しいメッセージからキーボードやブラウザーが候補に出すコードを選び、音声入力でもコードを読み上げて、コード全体が入るか確認する。
3. 「481 902」のように途中に空白かハイフンのあるコードを貼り付け、入って受け付けられるか確認する。
4. キーボードで1桁だけ直す。欄に分けている場合は、Shift + Tabでその欄に戻って打ち直す。ボタンを押すまで何も送られないことを確認する。
5. スクリーンリーダーで、欄または欄のまとまりがラベルと説明とともに読まれ、送信前にコードを読み返せるか確認する。

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

## 制限

このサンプルは、どのログインでも同じ結果になることを保証するものではありません。例はテキストメッセージで送るコードです。認証アプリやメールのコードも、同じように貼り付けを確かめてください。この基準は、コードを送る経路を指定していません。ログインする端末にコードが届くかどうかも対象外です。貼り付けを拒むパスワード欄や解かせるパズルなど、ログインのほかの部分も同じ基準の対象です。利用者が頼る人や道具と一緒に、ログイン全体を確認してください。

---

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