# ボタンに名前がなく、数を入力できない数量ステッパー

Source: https://easeweb.dev/learn/quantity-steppers
Topics: カスタム入力 > 数量の増減ボタンに名前があり、数値を直接入力できる (WCAG 4.1.2, 2.1.1)
The fix: 各ボタンに、何をするかを表す名前を視覚的に隠したテキストで付け、アイコンは読み上げから外します。そのうえで、表示されただけの数字を、ボタンで増減できるラベル付きの数値の入力欄に置き換えるか、ブラウザーの数値の入力欄だけを使います。数値の入力欄のキーをすべて取り消すことはせず、数量に決して入らない数文字だけを止めます。
Test with: keyboard, screen reader, touch
References: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F68 https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/ https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/number https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/stepUp

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

## 場面

$18のホーローマグを売るネットショップのページです。「カートに入れる」の上に数量のステッパーがあり、丸い−のボタン、数字の1、丸い＋のボタンが並んでいます。マウスなら問題なく使え、＋を3回押すと数字は4になります。スクリーンリーダーを起動してTabで移動すると、「ボタン」、もう一度「ボタン」と聞こえます。2つを見分ける手がかりは−と＋のアイコンだけで、どちらにも言葉はありません。間の数字はTabで止まらないので、押した後に今いくつなのかを伝えるものもありません。確かめるには、スクリーンリーダーの利用者はボタンを離れて、その行を読みに行く必要があります。数字を直接入力することもできません。カフェ用に10個買うなら、＋を9回押すことになります。

同じショップの別の作りでは、数量は「数量」とラベルの付いたふつうの数値の入力欄で、端にブラウザーの小さな上下の矢印があります。文字や「-3」が入らないように、開発者がTab以外のすべてのキーを取り消しています。Tabで届き、スクリーンリーダーは「数量、スピンボタン、1」と読みますが、4と入力しても何も起きません。本来ブラウザーが数を増減させる上下の矢印キーも効きません。数を変えられるのは、マウスで小さな矢印を押したときだけです。

## 確認できる問題

達成基準4.1.2は、すべての操作部品に、支援技術が読める名前があることを求めています。ボタンの名前は中身から決まりますが、ここでの中身はtitleも文字もないSVGだけです。そのため名前は空になり、スクリーンリーダーには役割しか読むものがありません。これは失敗例F68、つまりコード上で名前のない操作部品に当たります。−と＋は図形として描かれているだけで文字ではないため、ブラウザーには読み取れません。数字は役割のない文字だけの `span` なので、ページを1行ずつ読んだときにしか読まれず、ボタンと一緒には読まれません。コード上でボタンと結び付けるものもありません。

達成基準2.1.1は、マウスでできることはすべてキーボードでもできることを求めています。2つ目の作りは、本来なら欄だけでこの基準を満たせます。ブラウザーの数値の入力欄には、文字の入力と上下の矢印キーでの増減が初めから備わっているからです。しかしブラウザーがキー本来の動作をするのは、ページの `keydown` の処理が終わった後です。その処理で `preventDefault()` を呼ぶと、動作は取り消されます。そのため矢印キーも含め、どのキーを押しても何も起きません。欄の端のスピンの矢印はブラウザーがポインター用に描くもので、フォーカスを受けないため、数を設定する方法はマウスしか残りません。キーを止めるのは文法上正しいスクリプト1行にすぎず、HTMLの側では防げません。

## 利用者への影響

数量のステッパーは、カートへ進む途中にあります。読み上げられない、または数を設定できないステッパーがあると、購入はそこで止まるか、違う数のまま注文されてしまいます。カートの各行にも同じステッパーが置かれるため、行ごとに名前のないボタンが2つずつ増えます。

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

axeは1つ目の作りのボタンを名前がないもの（`button-name`）として報告しますが、間の数字を入力できないことは分かりません。数字の入った `span` は正しいマークアップだからです。2つ目の作りには、自動チェックは何の問題も見つけません。マークアップはラベル付きの数値の入力欄で、正しく書かれており、キーが止められていることはキーを押して初めて分かります。EaseWebの診断は、横にステッパーのボタンがない数値の入力欄ごとに、矢印キーを押して数字を入力し、どれでも値が変わらない欄を報告します。自動チェックの指摘を直すようにコード生成AIに頼むと、各ボタンを働きではなく絵柄で名付けたり（「＋」「マイナス」）、名前を付けただけで終わったりすることがあり、数字は入力できないまま残ります。数値の入力欄に文字が入らないようにしたいと頼むと、`keydown` を取り消す方法を提案することがあり、矢印キーと数字まで一緒に止めてしまいます。

## 修正前：表示されただけの数字を挟む、名前のないアイコンボタン

```html
<form class="buy" action="/basket" method="post">
  <span class="qty-label">数量</span>
  <div class="stepper">
    <button type="button" class="step" data-step="-1">
      <svg class="icon" viewBox="0 0 16 16">…</svg>
    </button>
    <span class="qty">1</span>
    <button type="button" class="step" data-step="1">
      <svg class="icon" viewBox="0 0 16 16">…</svg>
    </button>
  </div>
  <input type="hidden" name="quantity" value="1">
  <button type="submit">カートに入れる</button>
</form>
```

```js
const form = document.querySelector('.buy');
for (const button of form.querySelectorAll('.step')) {
  button.addEventListener('click', () => {
    const field = form.elements.quantity;
    const value = Math.min(10, Math.max(1, Number(field.value) + Number(button.dataset.step)));
    field.value = value;
    form.querySelector('.qty').textContent = value;
  });
}
```

各ボタンの中身はtitleのないSVGだけなので、名前は空になり、スクリーンリーダーは「ボタン」としか読みません。数量は `span` と隠しinputで、ボタンと一緒には読まれず、入力もできません。

## 修正前：キーを取り消す数値の入力欄

```html
<form class="buy" action="/basket" method="post">
  <label for="qty">数量</label>
  <input id="qty" name="quantity" type="number" min="1" max="10" value="1">
  <button type="submit">カートに入れる</button>
</form>
```

```js
const qty = document.querySelector('.buy').elements.quantity;
// 数はスピンの矢印でだけ変え、おかしな値が入力されないようにする
qty.addEventListener('keydown', (event) => {
  if (event.key !== 'Tab') event.preventDefault();
});
```

欄にはラベルがあり、正しく読み上げられます。しかし `keydown` の処理が、ブラウザーが動く前にTab以外のすべてのキーを取り消します。数字は入らず、上下の矢印キーでも増減しないため、数を変えるにはマウスでスピンの矢印を押すしかありません。

## 修正後：数値の入力欄を挟む、名前の付いたボタン

```html
<form class="buy" action="/basket" method="post">
  <label for="qty" class="qty-label">数量</label>
  <div class="stepper">
    <button type="button" class="step" data-step="-1">
      <svg class="icon" viewBox="0 0 16 16" aria-hidden="true">…</svg>
      <span class="visually-hidden">数量を減らす</span>
    </button>
    <input id="qty" name="quantity" type="number" min="1" max="10" value="1">
    <button type="button" class="step" data-step="1">
      <svg class="icon" viewBox="0 0 16 16" aria-hidden="true">…</svg>
      <span class="visually-hidden">数量を増やす</span>
    </button>
  </div>
  <button type="submit">カートに入れる</button>
</form>
```

```js
const form = document.querySelector('.buy');
const qty = form.elements.quantity;
for (const button of form.querySelectorAll('.step')) {
  button.addEventListener('click', () => {
    // stepUp()とstepDown()は、数をminとmaxの間に保つ
    if (button.dataset.step > 0) qty.stepUp();
    else qty.stepDown();
  });
}
```

各ボタンは「数量を減らす、ボタン」のように働きを伝え、アイコンは名前に何も加えないよう読み上げから外れます。数字は本物の入力欄になり、「数量、スピンボタン、1」と読まれ、10と直接入力でき、上下の矢印キーで増減できます。フォームが送信するのもこの欄なので、隠しinputは不要になります。`stepUp()` と `stepDown()` は `min` と `max` で止まるため、ボタンで1未満や10より多くにはなりません。丸いボタンのデザインはそのまま残せます。デザインがそのボタンだけを見せる場合は、ブラウザー自身のスピンの矢印をCSSで隠しても構いません。矢印キーは矢印の絵ではなく欄に備わっているので、引き続き使えます。

## 修正後：ブラウザーの数値の入力欄だけ

```html
<form class="buy" action="/basket" method="post">
  <label for="qty">数量</label>
  <input id="qty" name="quantity" type="number" min="1" max="10" value="1">
  <button type="submit">カートに入れる</button>
</form>
```

```js
const qty = document.querySelector('.buy').elements.quantity;
// 数量に決して入らない文字だけを止める。数字、矢印、編集のキーは使える
qty.addEventListener('keydown', (event) => {
  if (['e', 'E', '+', '-', '.', ','].includes(event.key)) event.preventDefault();
});
```

ブラウザーの欄がすべてを受け持ちます。文字の入力、上下の矢印キー、「数量、スピンボタン、1」と読まれる役割と値、そして `min` と `max` です。スクリプトが取り消すのは6文字だけです。指数の「e」、符号、小数点は、数値の入力欄が受け付けることはあっても、数量には決して入りません。これで、おかしな値を入れさせないという開発者のねらいも保てます。キーが止められていた欄にとって最も小さな修正で、ボタンは一つも要りません。ただし見た目は丸いボタンから変わります。残るのはデスクトップのブラウザーが欄の端に描く小さな矢印だけで、スマートフォンのブラウザーはその矢印も描かないことがあります。その場合、欄をタップすると数字入力用のキーボードが開きます。

## 修正後：ボタンに名前を付けるだけ

```html
<form class="buy" action="/basket" method="post">
  <span class="qty-label">数量</span>
  <div class="stepper">
    <button type="button" class="step" data-step="-1">
      <svg class="icon" viewBox="0 0 16 16" aria-hidden="true">…</svg>
      <span class="visually-hidden">数量を減らす</span>
    </button>
    <span class="qty">1</span>
    <button type="button" class="step" data-step="1">
      <svg class="icon" viewBox="0 0 16 16" aria-hidden="true">…</svg>
      <span class="visually-hidden">数量を増やす</span>
    </button>
  </div>
  <input type="hidden" name="quantity" value="1">
  <button type="submit">カートに入れる</button>
</form>
```

```js
const form = document.querySelector('.buy');
for (const button of form.querySelectorAll('.step')) {
  button.addEventListener('click', () => {
    const field = form.elements.quantity;
    const value = Math.min(10, Math.max(1, Number(field.value) + Number(button.dataset.step)));
    field.value = value;
    form.querySelector('.qty').textContent = value;
  });
}
```

1つ目の作りへの最も小さな変更で、自動チェックの指摘が求めるのもこれです。各ボタンは「数量を減らす、ボタン」「数量を増やす、ボタン」と読まれ、どちらも4.1.2を満たします。ただし、これだけでは足りません。数量は表示されただけの数字のままなので誰も入力できず、押した後もフォーカスはボタンに残り、数がいくつになったかを伝えるものがありません。テンプレートをすぐに変えられないときの第一歩として使い、変えられるようになったらspanを入力欄に置き換えてください。

## どの方法を選ぶか

3つともボタンに名前を付けます。初めの2つは、さらに数量を、入力と矢印キーでの増減ができる欄にします。数値の入力欄を挟む名前の付いたボタンは、デザインどおりの見た目のまま、数を入力できるようにします。大きな丸いボタンが大切な場合に選びます。スマートフォンでは、ステッパーの中でいちばん押しやすいのがこのボタンなので、たいていは大切です。ブラウザーの数値の入力欄だけの方法はコードが最も少なく、キーが止められていた欄への素直な修正です。デスクトップのカートの表や、一度に何十個も注文する業務用の注文フォームのように、ふつうの入力欄がデザインに合う場合に選びます。ボタンに名前を付けるだけの方法は最も手早く、自動チェックの指摘も消えますが、数字は入力できないままです。修正そのものではなく、1つ目の方法へ向かう途中の一歩と考えてください。

## 実装の判断

ページに組み込んだ後のステッパーの動作は、次の点で決まります。

- カートでの名前：どの行にもステッパーがあると、ボタンの一覧には「数量を減らす」「数量を増やす」が何度も並ぶ。名前と欄のラベルに商品名を入れる（「数量を増やす、ホーローマグ」）。見えている言葉の後に、視覚的に隠したテキストで加える。
- 最小と最大の数量：1と10でもボタンは有効のままにする。そこでは `stepDown()` と `stepUp()` が何もしないだけだからである。フォーカスのあるボタンを無効にすると、フォーカスはページ全体に移ってしまう。[見つからず理由も分からない無効なボタン](/ja/learn/disabled-controls)を参照。
- 押したときに伝わること：フォーカスはボタンに残るため、スクリーンリーダーは欄の新しい数を読まない。ただし欄は、Tabの順序でボタンのすぐ隣にある。利用者が気づきにくいとテストで分かったら、「数量4」のような短い控えめなステータスで補う。「カートに入れる」でカートがどう変わったかを伝えることは、カートへの追加などの操作結果を音声で知らせるという、別のトピックになる。
- 範囲外の数：`max` が10の欄にも、30と入力することはできる。フォームを送信するとブラウザーが受け付けず、理由を伝える。ページが自分で数を直す場合は、黙って変えず、欄の横でそう伝える（「残り10個」）。

スクリプトがほかのキーと一緒にTabまで取り消すと、フォーカスは欄からまったく出られなくなります。これは別の問題です。[フォーカスが抜け出せないエディターや埋め込み部品](/ja/learn/keyboard-traps)を参照してください。文字を入れさせないために、欄を `readonly` にすることも避けます。止められたキーと同じく、入力も矢印キーも使えなくなり、ほとんどのブラウザーではスピンの矢印まで効かなくなります。

ショップのテーマやデザインシステムのステッパーはさまざまです。ボタンに名前はあっても数字はただの文字で表示するもの、入力欄はあってもアイコンに名前がないもの、文字を入れさせないためにキーを止めるものもあります。コンポーネントの説明をうのみにせず、ブラウザーの開発者ツールのアクセシビリティ欄で、各部分が実際に何として伝わっているかを確かめてください。

## 修正の確認

1. ブラウザーの開発者ツールのアクセシビリティ欄で各ボタンを調べ、名前が働きを表していること、アイコンが名前に何も加えていないことを確認する。
2. マウスを使わずに、Tabで数量へ移動し、新しい数を入力してから上下の矢印キーを押す。どれでも数が変わること、Tabで「カートに入れる」へ進めることを確認する。
3. Tabで各ボタンへ移動し、Enter、次にSpaceで押す。数が1つずつ減ったり増えたりし、最小と最大の数量で止まり、フォーカスがボタンに残ることを確認する。
4. スクリーンリーダーでステッパーをTabで移動し、各ボタンでは名前と「ボタン」が、欄ではラベル、役割、数が読み上げられることを確認する。
5. スマートフォンで欄をタップし、数字入力用のキーボードが開くことを確認する。次にスクリーンリーダーをオンにしてステッパーをスワイプでたどり、各ボタンの名前と、欄のラベルと数が聞こえることを確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。ステッパーのコンポーネントやテーマを変更した際は、再度確認します。

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。数値の入力欄の役割はスクリーンリーダーによって呼び方が異なり、ラベルより先に数を読むものもあります。人数、泊数、時刻を設定するステッパーも同じ作り方ですが、サイズのように言葉を順に切り替えるステッパーは別の部品で、selectやラジオボタンのグループを使います。トラックの上を動かす部品なら[ドラッグでしか動かない範囲スライダー](/ja/learn/sliders)を、ヘッダーの検索やメニューのような、ページのほかの場所のアイコンボタンなら[名前がないアイコンだけのボタン](/ja/learn/icon-buttons)を参照してください。対象とする支援技術で、商品ページからカートまでの操作全体を確認してください。

---

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