# 入力した番号や貼り付けた番号が変わってしまう入力マスク

Source: https://easeweb.dev/learn/input-masks
Topics: フォーム > 電話番号やカード番号の欄が、どんな形で入力しても貼り付けても受け付ける (best practice; related to WCAG 3.3.2)
The fix: マスクを使わず、番号を入力されたまま受け取ります。欄はふつうの入力欄にしてヒントに例を示し、送信するときに、間に何が入っていても数字だけを取り出し、先頭の国番号1を取り除いて確かめます。区切った見た目を残したい場合は、入力中には書き換えず、欄を離れたときに(212) 555-0147の形で書き戻します。
Test with: keyboard, touch, autofill, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html https://www.w3.org/WAI/WCAG22/Techniques/general/G89 https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html https://design-system.service.gov.uk/patterns/telephone-numbers/ https://www.filamentgroup.com/lab/politespace https://github.com/RobinHerbots/Inputmask/issues/981 https://github.com/uNmAnNeR/imaskjs/issues/800 https://github.com/nosir/cleave.js/issues/193

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

## 場面

ネットショップの購入手続きで、荷物を発送したときにSMSで知らせるため、電話番号を入力します。欄の中には (___) ___-____ と形式が描かれていて、何を入力すればよいかが一目で分かり、入力すると数字が空いた枠を埋めていきます。開発者は、よく使われる入力マスクのライブラリーを1行で組み込むか、数字が入るたびに括弧とハイフンを足す数行のコードを自分で書いて、この欄を作ります。

利用者が、連絡先に「+1 212 555 0147」と保存してある自分の番号をコピーして貼り付けます。欄には「(121) 255-5014」と表示されます。電話番号らしく見えるので「次へ」を押すと、お知らせは別の人に届きます。マスクには10個の枠しかなく、最初に来た10桁の数字で順に埋めるためです。国番号の1は市外局番の最初の数字になり、最後の7は消えます。先に「+1」と入力しても同じで、+は何の知らせもなく捨てられ、1が最初の枠に入ります。2026年10月に、Chromiumで、Inputmask 5.0、IMask 7.6、Maska 3.2と、下の手書きのコードで試したところ、どれも貼り付けた番号を「(121) 255-5014」にしました。IMaskのイシュートラッカーにも、+7のマスクで、先頭に8を付けて貼り付けた番号がおかしくなるという同じ種類の報告があります。

作り方の違いによって、ほかにも次のように問題が出ます。

- ライブラリーの場合は、欄にフォーカスが移るとすぐに、パターンが欄の値になります。スクリーンリーダーは括弧と空いた枠を欄の中身として読み上げます。Inputmaskの利用者からは、「underline underline underline…」と読み上げられるという報告があります。
- 手書きのコードの場合は、変更のたびに値を組み立て直すため、カーソルが末尾へ飛びます。4桁目の間違いに気づいて消し、正しい数字を入力すると、その数字は最後に付きます。「(212) 550-1479」になってしまいます。

## 確認できる問題

ここで満たしていない達成基準はなく、このガイドで扱うのはベストプラクティスです。達成基準3.3.2「ラベル又は説明」は、入力が必要な場所にラベルや説明を求めており、解説書（Understanding）は、入力する形式を示してもよいとしています（達成方法G89、形式と例を示す）。2つの修正前は、どちらもこれを満たします。欄にはラベルがあり、ライブラリーのパターンは形式まで示しています。番号を複数の書き方で受け付けることは、WCAGのどこにも求められていません。

このガイドの助言の根拠は、デザインシステムや利用者テストで得られた知見です。GOV.UKの電話番号のパターンは、入力マスクを避けるよう勧めています。利用者が好きな書き方で番号を入力しにくくなり、ほかの場所から番号を写して確かめにくくなるためです。そして、スペース、ハイフン、ダッシュ、括弧を受け付け、国番号と市外局番にも対応するよう求めています。Filament Groupは、空いた枠はフォーカスしたときに読み上げられても「意味のない雑音」にしかならず、入力中にスクリプトが値を書き換えると、スクリーンリーダーが値の切れ端を読み上げることがあると述べています。

問題は、マスクが受け取った文字をどう扱うかにあります。マスクは決まった枠の数を持つ1つの形式しか知らず、文字を順に読み、+やスペースのように形式に合わない文字は捨て、数字を次の枠に入れて、枠がなくなったところで止まります。別の書き方の番号は収まらず、何を捨てたかは利用者に伝わりません。カーソルが飛ぶのはブラウザーの動作です。スクリプトが入力欄の値を設定すると、HTMLの仕様によってカーソルはテキストの末尾に移るため、変更のたびに値を組み立て直すハンドラーは、カーソルを元の位置から動かしてしまいます。

1か所だけ、達成基準に近い部分があります。達成基準3.3.1「エラーの特定」は、ページが検出した入力エラーをテキストで説明することを求めており、解説書は、値が大きすぎたり小さすぎたりして、ページのコードが許される範囲に自動で変える場合も、そのエラーは説明しなければならないとしています。+と数字1つを黙って捨てるマスクは、これに近い状態です。下の修正はどちらも何も捨てないため、この問題は起きません。

## 利用者への影響

マスクの形式どおりに1桁ずつ番号を入力する人以外は、誰でもこの問題に出会います。番号を貼り付ける人、ブラウザーに自動入力させる人、カードに印刷されたとおりに書く人です。連絡先アプリに保存した番号や、ブラウザーが自動入力する番号には、先頭に国番号が付いていることがあるため、貼り付けた番号が合わないのは、まれなことではありません。

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

欄にはラベルがあり、ページはaxeのチェックを通り、スキャンは何も貼り付けません。EaseWebの診断は、電話番号の欄には+1 212 555 0147を、カード番号の欄には4242 4242 4242 4242を、スクリプトで貼り付け、フォームは送信せずに値を元に戻します。数字が変わってしまった欄と、値が空いた枠のパターンになっている欄を、人が試す項目として挙げます。達成基準の不適合ではないため、問題としては報告しません。カーソルがどこへ行くか、数字を入れるたびにスクリーンリーダーが何を読み上げるかは、人が確かめる必要があります。コーディングアシスタントに「(999) 999-9999の形式に整形する電話番号欄」を頼むと、どちらの修正前のコードも返ってくることがあります。

## 修正前：欄の中にパターンを入れるマスクのライブラリー

```html
<form>
  <label for="tel">電話番号</label>
  <input id="tel" name="tel" type="tel" autocomplete="tel">
  <button type="submit">次へ</button>
</form>
```

```js
import Inputmask from 'inputmask';

const tel = document.getElementById('tel');

// 括弧、スペース、ハイフンと、10桁の数字それぞれの枠。
Inputmask('(999) 999-9999').mask(tel);
```

- 欄にフォーカスが移ると、値が (___) ___-____ というパターンになり、スクリーンリーダーはそれを欄の中身として読み上げる。
- マスクには10個の枠しかない。+1 212 555 0147を貼り付けると、最初の10桁で枠が埋まって「(121) 255-5014」と表示され、最後の7は消える。
- +は捨てられ、番号が変わったことは何も伝えられない。

## 修正前：手書きの整形コード

```html
<form>
  <label for="tel">電話番号</label>
  <input id="tel" name="tel" type="tel" autocomplete="tel">
  <button type="submit">次へ</button>
</form>
```

```js
const tel = document.getElementById('tel');

// 入力に合わせて番号を整形する：(212) 555-0147
tel.addEventListener('input', () => {
  const d = tel.value.replace(/\D/g, '').slice(0, 10);
  tel.value = d.length > 6 ? `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}`
    : d.length > 3 ? `(${d.slice(0, 3)}) ${d.slice(3)}`
    : d;
});
```

同じ欄で、整形を自分で書いた場合です。

- 変更のたびに値を設定するため、カーソルが末尾へ飛ぶ。間違った4桁目を消して正しい数字を入力すると、その数字は最後に入り、「(212) 550-1479」になる。
- 先頭に+1の付いた番号を貼り付けると、ライブラリーと同じく最初の10桁だけが残り、「(121) 255-5014」になる。
- 数字以外の文字は、入力したそばから何の知らせもなく捨てられる。

## 修正後：マスクを使わず、送信するときに数字を確かめる

```html
<form>
  <label for="tel">電話番号</label>
  <p id="tel-hint">市外局番を含めて入力してください。例：(212) 555-0147</p>
  <input id="tel" name="tel" type="tel" autocomplete="tel"
    aria-describedby="tel-hint tel-error">
  <p id="tel-error"></p>
  <button type="submit">次へ</button>
</form>
```

```js
const form = document.querySelector('form');
const tel = document.getElementById('tel');
const error = document.getElementById('tel-error');

// 間に何が入っていても、番号の10桁の数字を返す。10桁でなければnull。
function digitsOf(value) {
  let digits = value.replace(/\D/g, '');
  if (digits.length === 11 && digits.startsWith('1')) digits = digits.slice(1);
  return digits.length === 10 ? digits : null;
}

form.addEventListener('submit', (event) => {
  const digits = digitsOf(tel.value);
  tel.setAttribute('aria-invalid', String(!digits));
  error.textContent = digits ? '' : '市外局番を含めて、電話番号を10桁で入力してください。';
  if (digits) tel.value = digits;
  else event.preventDefault();
});
```

1. マスクを外し、スペース、括弧、ハイフン、ドットが入っていても、入力された番号も貼り付けられた番号も、そのまま欄に残す。
2. ラベルの下に例を示し、`aria-describedby` で欄に関連付ける。例は欄と一緒に読み上げられ、入力中も画面に残る。
3. 送信するときに数字だけを取り出し、11桁なら先頭の1を取り除く。「+1 212 555 0147」も「(212) 555-0147」も「212.555.0147」も、同じ10桁になる。
4. 桁数が合わない値には、何を入力すればよいかを伝えるメッセージを、欄に関連付けて出す。
5. 10桁の数字を送信する。リクエストはこのスクリプトを通らずに届くこともあるため、サーバーでも同じように数字を取り出す。

2つの修正前をどちらも直します。入力中に欄を書き換えるものはないので、カーソルは利用者が置いた場所から動かず、貼り付けた番号も欠けずに入ります。3.3.2は修正前と同じく満たし、形式は言葉で示されるようになります。

## 修正後：欄を離れたときに数字を区切る

```html
<form>
  <label for="tel">電話番号</label>
  <p id="tel-hint">市外局番を含めて入力してください。例：(212) 555-0147</p>
  <input id="tel" name="tel" type="tel" autocomplete="tel"
    aria-describedby="tel-hint tel-error">
  <p id="tel-error"></p>
  <button type="submit">次へ</button>
</form>
```

```js
const form = document.querySelector('form');
const tel = document.getElementById('tel');
const error = document.getElementById('tel-error');

// 間に何が入っていても、番号の10桁の数字を返す。10桁でなければnull。
function digitsOf(value) {
  let digits = value.replace(/\D/g, '');
  if (digits.length === 11 && digits.startsWith('1')) digits = digits.slice(1);
  return digits.length === 10 ? digits : null;
}

// 番号は、入力中ではなく、欄を離れたときに区切る。
tel.addEventListener('change', () => {
  const digits = digitsOf(tel.value);
  if (digits) tel.value = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
});

form.addEventListener('submit', (event) => {
  const digits = digitsOf(tel.value);
  tel.setAttribute('aria-invalid', String(!digits));
  error.textContent = digits ? '' : '市外局番を含めて、電話番号を10桁で入力してください。';
  if (digits) tel.value = digits;
  else event.preventDefault();
});
```

1. 変更のたびに番号を整形していたハンドラーを外す。
2. ラベルの下に同じ例を示し、欄に関連付ける。
3. 欄を離れたとき、数字がそろって1つの番号になっていれば、「(212) 555-0147」の形で書き戻す。そろっていない値は入力されたまま残し、メッセージで説明する。
4. 送信するときは、1つ目の修正と同じように数字を確かめて送信する。

2つの修正前をどちらも直し、3.3.2も満たします。欄が変わるのは利用者が入力を終えたあとだけなので、入力中にカーソルは動かず、貼り付けた番号も欠けません。欄に戻ってきたときには、区切った番号が表示され、スクリーンリーダーもその形で読み上げます。

## どの方法を選ぶか

どちらも、番号をどう入力しても、どう貼り付けても受け付けます。使うべきなのは、マスクを使わない方法です。確かめる処理は同じでコードが少なく、番号は利用者が書いたとおりに表示されます。GOV.UK Notifyのチームは、書き直された番号を見せられて戸惑う利用者がいたと報告しています。欄を離れたときに区切る方法は、カード番号を4桁ずつ区切るように、どの番号も同じ形で見せたいデザインのためのものです。欄を変えるのは入力を終えたあとの1回だけで、カーソルを動かすことはありません。

## 実装の判断

マスクのライブラリーを残すしかない場合、パターンの表示を止めれば、空いた枠は値から消えます（Inputmaskでは `showMaskOnFocus: false` と `showMaskOnHover: false`、IMaskでは既定の `lazy: true`）。ただし、これは修正にはなりません。私たちのテストでは、パターンを表示しないIMaskでも、貼り付けた+1 212 555 0147は「(121) 255-5014」になりました。

`maxlength` は付けません。「(212) 555-0147」に合わせた14のように、形式に合わせた値を付けると、貼り付けた+1 212 555 0147が同じように途中で切れます。GOV.UKのテキスト入力のガイダンスも、厳しい文字数の制限は、利用者がいつもの書き方で値を入力するのを妨げると警告しています。桁数は、送信するときに数字の数で確かめます。

`type="tel"` と `autocomplete="tel"` は残します。前者はタッチ画面で電話番号用のキーパッドを出し、どんな文字も受け付けます。後者はブラウザーに番号を自動入力させます。自動入力される番号には国番号が付いていることもあり、上の確認はそれも想定しています。`type="number"` は数量のための型で、スペースや括弧を受け付けないため、電話番号には使いません。

カード番号も同じです。スペースがあってもなくても受け付け、`autocomplete="cc-number"` と `inputmode="numeric"` を付けます。カードに印刷されたとおりの形で見せたいデザインなら、欄を離れたときに4桁ずつ区切ります。「[入力をはじかず、こちらで変換する](/ja/learn/rules-of-thumb#convert-not-reject)」を参照してください。MM/DD/YYYYのような日付のマスクも同じ問題です。入力された日付の受け取り方は「[入力もキーボード操作もできない日付ピッカー](/ja/learn/date-picker)」で扱っています。

複数の国の番号を受け付けるショップでは、+と国番号を残す必要があり、各国の番号の決まりを知っているlibphonenumberのようなライブラリーを、キーを押すたびではなく、送信するときに使います。

## 修正の確認

1. 欄に+1 212 555 0147を貼り付けてフォームを送信する。ページは2125550147を送信し、同じ数字の番号を表示し直す。
2. 番号を「(212) 555-0147」「212.555.0147」「212 555 0147」の形でそれぞれ入力して送信する。どれも同じ番号として受け付けられる。
3. 番号を入力し、カーソルを4桁目に戻して消し、別の数字を入力する。カーソルは置いた場所から動かず、新しい数字はそこに入る。
4. スクリーンリーダーを起動し、Tabキーで欄に移動する。ラベルとヒントが読み上げられ、括弧や空いた枠が欄の値として読み上げられない。数字をいくつか入力し、入力したとおりに読み返されることを聞いて確かめる。
5. 9桁の数字を入力して送信する。市外局番を含めて10桁で入力するよう、メッセージで伝えられる。
6. テスト用のリクエストなどで、ページのスクリプトを通さずに+1 212 555 0147をサーバーへ送る。サーバーは同じ10桁の数字を残すか、何を入力すればよいかを伝えるメッセージで値をはじく。

OS、ブラウザー、支援技術のバージョン、ビルド、日付、各手順の実際の結果を記録してください。共通の電話番号欄やその整形の処理を変えたら、もう一度確認します。

## 制限

これは出発点となる実装例で、すべてのアプリケーションでの動作を保証するものではありません。例は、10桁で、国番号1が付くこともある米国の番号です。ほかの国では桁数が異なり、国をまたぐフォームでは+を残します。番号を区切りごとに3つの欄に分けて尋ねるのは別の問題で、「[分割した欄それぞれに名前がない日付や電話番号の入力欄](/ja/learn/split-fields)」で扱っています。日本語入力で全角の数字が入る欄の問題は、「[全角数字を受け付けず、理由も示さない入力欄](/ja/learn/full-width-digits)」で扱っています。送信に失敗したあとのメッセージの伝え方は、「[直し方がわからないフォームエラー](/ja/learn/accessible-form-errors)」で扱っています。

---

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