# 「ボタン」としか読まれない顔アイコンの評価

Source: https://easeweb.dev/learn/face-ratings
Topics: カスタム入力 > 星や顔の評価をキーボードで選べ、それぞれの値が伝わる (WCAG 2.1.1, 4.1.2, 1.3.1, 1.1.1)
The fix: 各顔に値を言葉で名前として付け、設問をグループの名前にします。方法は二つあります。デザインを変えずに、値をテキストとしてボタンに入れる方法（両端の言葉は両端のボタンの中へ移し、中央の3つには視覚的に隠した言葉を入れる）と、すべての顔の下に言葉を書き、ボタンの中に入れる方法です。どちらの場合も、設問は顔を囲むfieldsetのlegendにします。
Test with: screen reader, speech input, keyboard, touch
References: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F68 https://www.w3.org/TR/accname-1.2/ https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/ https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/fieldset

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

## 場面

あるレシピサイトに、おすすめのレシピが表示され、その下の小さなカードが「このおすすめはいかがですか？」と尋ねています。しかめ面から満面の笑みまで、丸い顔が5つ並び、最初の顔の下に「とても悪い」、最後の顔の下に「とても良い」と書かれています。顔ならフォームより気軽に答えられ、ページの隅にも収まります。顔をクリックすると回答が送られます。

スクリーンリーダーを有効にして、Tabでカードに入ります。読み上げは「ボタン」、また「ボタン」と、顔ごとに5回続きます。どの顔が何を表すのかも、どちらの端が良いのかも伝わりません。カードを先頭から読むと、「このおすすめはいかがですか？」の後に「ボタン」が5回続きます。「とても悪い」「とても良い」は、すべてのボタンの後でようやく2行のテキストとして読まれ、どちらの端の顔にも結び付いていません。音声操作の利用者が「とても良いをクリック」と言っても、その名前のボタンはないので何も起きません。

## 確認できる問題

各顔は本物の `button` なので、Tabで届き、Enterキーで押せます。欠けているのは名前です。ブラウザーはボタンの名前を、`aria-labelledby`、`aria-label`、ボタンに結び付いた `label`、ボタン内のテキストの順に探し、最後の手段として `title` を使います。ここでボタンの中にあるのは `aria-hidden="true"` で隠した顔の絵だけで、`title` は付いていても空です。どこからも名前が得られず、ボタンは役割だけで読み上げられます。空の `title` からは、共通のアイコンボタンのコンポーネントが使われていることがうかがえます。名前を入れる場所はあるのに、このカードでは空のままです。

このため、3つの達成基準を満たしていません。4.1.2 名前・役割・値：各顔は役割はあっても名前のない操作部品で、押すと何が起きるかをソフトウェアが伝えられません。WCAGはこれを失敗例F68として挙げています。1.1.1 非テキストコンテンツ：顔は回答そのものを表す絵で、操作部品として働く絵には、目的を伝える代替テキストが必要です。ここでは「悪い」「ふつう」「良い」の違いは、口の形で描き分けてあるだけです。1.3.1 情報及び関係性：見た目では、上の設問が5つの顔にかかっていること、「とても悪い」が最初の顔の、「とても良い」が最後の顔の言葉であることが分かります。ところがコードでは、設問はどこにも結び付かない `div` で、顔はその下にまとめられておらず、2つの言葉はすべてのボタンの後ろにある別の `div` に入っています。そのため最後に、どの顔にも属さないまま読まれます。

## 利用者への影響

短い感想のカードは、動画の後、サポートのチャットの後、おすすめの後など、何かの終わりに出てきます。顔に名前がないと、見えない人は質問を飛ばすか、当てずっぽうで顔を押すことになります。その人たちの回答は、抜け落ちるか当て推量になり、顔の意味どおりの回答は、顔が見える人からしか集まりません。

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

axeは各顔を `button-name` として報告します。中身が `aria-hidden` で隠されたものだけで、`title` も空のボタンには名前がないからです。EaseWebの診断もaxeを実行し、5つの顔を `axe:button-name` として報告します。自動チェックで分かるのはここまでです。名前が値を伝えているかは判断できず、`aria-label="しかめ面"`、アイコンのファイル名の `"face-2"`、`"1"` でも合格になります。設問がどこにも結び付いていないことや、両端の言葉がボタンの後ろに取り残されていることも分かりません。指摘を直すように頼むと、コード生成AIは、顔に絵の説明で名前を付けたり、ボタンではなく隠した `svg` に `aria-label` を付けたりすることがあります。後者では、隠した要素は名前に何も加えないため、ボタンは名前のないままです。また、下の言葉から両端の2つだけに名前を付け、中央の3つを空のまま残すこともあります。スクリーンリーダーでTabを押して顔を移動し、それぞれが値を読み上げるか確かめてください。

## 修正前：titleが空の顔

```html
<form class="survey" action="/feedback" method="post">
  <div>
    <div class="question">このおすすめはいかがですか？</div>
    <div class="faces">
      <button class="face" name="answer" value="1" title="">
        <svg aria-hidden="true">
          <use href="#face-very-bad"></use>
        </svg>
      </button>
      <button class="face" name="answer" value="2" title="">
        <svg aria-hidden="true">
          <use href="#face-bad"></use>
        </svg>
      </button>
      <button class="face" name="answer" value="3" title="">
        <svg aria-hidden="true">
          <use href="#face-okay"></use>
        </svg>
      </button>
      <button class="face" name="answer" value="4" title="">
        <svg aria-hidden="true">
          <use href="#face-good"></use>
        </svg>
      </button>
      <button class="face" name="answer" value="5" title="">
        <svg aria-hidden="true">
          <use href="#face-very-good"></use>
        </svg>
      </button>
    </div>
    <div class="ends">
      <div>とても悪い</div>
      <div>とても良い</div>
    </div>
  </div>
</form>
```

```css
.survey {
  inline-size: fit-content;
  max-inline-size: 100%;
  padding: 1rem 1.25rem;
  border-radius: 0.75rem;
  background: #ffffff;
  box-shadow: 0 2px 12px rgb(0 0 0 / 0.15);
}
.question {
  margin: 0 0 0.75rem;
  padding: 0;
  font-weight: bold;
}
.faces {
  display: flex;
  gap: 0.5rem;
}
/* 顔：36pxの絵を囲む48pxの丸いボタン。#1f2937、白に対して14.68:1 */
.face {
  display: grid;
  place-items: center;
  inline-size: 3rem;
  block-size: 3rem;
  margin: 0;
  border: 0;
  border-radius: 50%;
  padding: 0;
  background: none;
  color: #1f2937;
  font: inherit;
  cursor: pointer;
}
.face svg {
  inline-size: 2.25rem;
  block-size: 2.25rem;
}
/* 両端の下の言葉：#4b5563、7.56:1 */
.ends {
  display: flex;
  justify-content: space-between;
  margin-top: 0.25rem;
  font-size: 0.8125rem;
  color: #4b5563;
}
```

各ボタンの中身は、支援技術から隠した絵と空の `title` だけなので、5つのどれにも名前がありません。設問と2つの言葉はボタンとは別の `div` で、どのボタンにも結び付いていません。画面の上では顔の列を挟んでいますが、コードの上では列の前後にあるただのテキストです。

## 修正後：デザインはそのままに、値をボタンに入れる

```html
<form class="survey" action="/feedback" method="post">
  <fieldset>
    <legend class="question">このおすすめはいかがですか？</legend>
    <div class="faces">
      <button class="face" name="answer" value="1">
        <svg aria-hidden="true">
          <use href="#face-very-bad"></use>
        </svg>
        <span class="end">とても悪い</span>
      </button>
      <button class="face" name="answer" value="2">
        <svg aria-hidden="true">
          <use href="#face-bad"></use>
        </svg>
        <span class="visually-hidden">悪い</span>
      </button>
      <button class="face" name="answer" value="3">
        <svg aria-hidden="true">
          <use href="#face-okay"></use>
        </svg>
        <span class="visually-hidden">ふつう</span>
      </button>
      <button class="face" name="answer" value="4">
        <svg aria-hidden="true">
          <use href="#face-good"></use>
        </svg>
        <span class="visually-hidden">良い</span>
      </button>
      <button class="face" name="answer" value="5">
        <svg aria-hidden="true">
          <use href="#face-very-good"></use>
        </svg>
        <span class="end">とても良い</span>
      </button>
    </div>
  </fieldset>
</form>
```

```css
.survey {
  inline-size: fit-content;
  max-inline-size: 100%;
  padding: 1rem 1.25rem;
  border-radius: 0.75rem;
  background: #ffffff;
  box-shadow: 0 2px 12px rgb(0 0 0 / 0.15);
}
/* min-inline-size：これがないと、fieldsetは中身の幅より狭くならない */
fieldset {
  min-inline-size: 0;
  margin: 0;
  border: 0;
  padding: 0;
}
.question {
  margin: 0 0 0.75rem;
  padding: 0;
  font-weight: bold;
}
/* 列の下に、両端の言葉を置く余白 */
.faces {
  display: flex;
  gap: 0.5rem;
  padding-block-end: 1.25rem;
}
/* 顔：36pxの絵を囲む48pxの丸いボタン。#1f2937、白に対して14.68:1 */
.face {
  position: relative;
  display: grid;
  place-items: center;
  inline-size: 3rem;
  block-size: 3rem;
  margin: 0;
  border: 0;
  border-radius: 50%;
  padding: 0;
  background: none;
  color: #1f2937;
  font: inherit;
  cursor: pointer;
}
.face svg {
  inline-size: 2.25rem;
  block-size: 2.25rem;
}
/* 両端の下の言葉。今はそれぞれのボタンの中にある：#4b5563、7.56:1 */
.end {
  position: absolute;
  inset-block-start: calc(100% + 0.25rem);
  font-size: 0.8125rem;
  color: #4b5563;
  white-space: nowrap;
}
.face:first-child .end {
  inset-inline-start: 0;
}
.face:last-child .end {
  inset-inline-end: 0;
}
.visually-hidden {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
```

カードの見た目はデザインどおりで、顔も、両端の下の2つの言葉も変わりません。各ボタンには値がテキストとして入ったので、スクリーンリーダーは「とても悪い、ボタン」から「とても良い、ボタン」まで読み上げ、音声操作ではその言葉を言えば顔が押されます。両端の言葉は、最初と最後のボタンの中へ移り、表示は今までどおりボタンの下です。画面の言葉がそのままボタンの名前になり、列の後ろで単独に読まれることもなくなります。中央の3つには、視覚的に隠したテキストで言葉を入れています。設問は顔を囲む `fieldset` の `legend` にしたので、スクリーンリーダーは通常、Tabでグループに入るときに「このおすすめはいかがですか？、グループ」のように名前を読み上げます。何の名前にもなっていなかった空の `title` は外しました。

## 修正後：すべての顔の下に言葉を書く

```html
<form class="survey" action="/feedback" method="post">
  <fieldset>
    <legend class="question">このおすすめはいかがですか？</legend>
    <div class="faces">
      <button class="face" name="answer" value="1">
        <svg aria-hidden="true">
          <use href="#face-very-bad"></use>
        </svg>
        とても悪い
      </button>
      <button class="face" name="answer" value="2">
        <svg aria-hidden="true">
          <use href="#face-bad"></use>
        </svg>
        悪い
      </button>
      <button class="face" name="answer" value="3">
        <svg aria-hidden="true">
          <use href="#face-okay"></use>
        </svg>
        ふつう
      </button>
      <button class="face" name="answer" value="4">
        <svg aria-hidden="true">
          <use href="#face-good"></use>
        </svg>
        良い
      </button>
      <button class="face" name="answer" value="5">
        <svg aria-hidden="true">
          <use href="#face-very-good"></use>
        </svg>
        とても良い
      </button>
    </div>
  </fieldset>
</form>
```

```css
.survey {
  inline-size: fit-content;
  max-inline-size: 100%;
  padding: 1rem 1.25rem;
  border-radius: 0.75rem;
  background: #ffffff;
  box-shadow: 0 2px 12px rgb(0 0 0 / 0.15);
}
/* min-inline-size：これがないと、fieldsetは中身の幅より狭くならない */
fieldset {
  min-inline-size: 0;
  margin: 0;
  border: 0;
  padding: 0;
}
.question {
  margin: 0 0 0.75rem;
  padding: 0;
  font-weight: bold;
}
.faces {
  display: flex;
  gap: 0.25rem;
}
/* 顔と、その下の言葉：36pxの絵と13pxの文字。#1f2937、白に対して14.68:1 */
.face {
  display: grid;
  justify-items: center;
  align-content: start;
  gap: 0.25rem;
  flex: 0 1 4rem;
  margin: 0;
  border: 0;
  border-radius: 0.5rem;
  padding: 0.375rem 0.125rem;
  background: none;
  color: #1f2937;
  font: inherit;
  font-size: 0.8125rem;
  line-height: 1.2;
  text-align: center;
  cursor: pointer;
}
.face svg {
  inline-size: 2.25rem;
  block-size: 2.25rem;
}
```

こちらは見た目が変わります。どの顔の下にも「とても悪い」「悪い」「ふつう」「良い」「とても良い」と言葉が入り、その分だけ列が少し広くなります。言葉はボタンのテキストなので、そのままボタンの名前にもなります。画面に見える言葉を、スクリーンリーダーはそのまま読み上げ、音声操作もその言葉を聞き取ります。隠したテキストと見た目を揃え続ける手間もありません。設問は、一つ目の方法と同じくグループの `legend` です。顔の表情を読み取らなくても、どの顔でも選べるようになります。

## どの方法を選ぶか

どちらも3つの達成基準を満たします。値をボタンに入れる方法は、カードをデザインどおりに保てるので、デザインに広げる余地がない場合に向いています。すべての顔の下に言葉を書く方法は、普段の見た目が変わりますが、カードを読むすべての人に各顔の意味が伝わります。顔の表情を読み取りにくい人、拡大鏡で顔を一つずつ見ている人、このサイトで中央の顔が何を意味するのか迷う人にも伝わります。カードに余裕があるならこちらを選んでください。幅の狭いスマートフォンでは、「とても良い」が顔の下に収まるかをデザインファイルで確かめるか、2行で描いておきます。

## 実装の判断

この顔は押した瞬間に回答を送るので、ボタンにしています。一度押せば一つの回答が送られる、実際のカードと同じ動きです。尺度が、後でまとめて送る長いフォームの設問の一つである場合や、送る前に回答を変えられる場合は、ラジオボタンのグループとして作ってください。星の場合の作り方は[キーボードでもスクリーンリーダーでも選べない星評価](/ja/learn/star-ratings)で示しています。顔も同じように、ラジオボタンの上に描けます。作り方によって、使うキーも変わります。ボタンの顔はTabで一つずつ移動し、EnterキーかSpaceキーでその回答を送ります。ラジオボタンのグループではTabが止まるのは一度だけで、矢印キーで選択が移ります。そのため、ラジオボタンのグループは選んだだけで送信してはいけません。矢印キーは通り過ぎる顔を次々にチェックするので、キーボードで「とても悪い」から「良い」へ移ろうとすると、最初の一押しで「とても悪い」が送られてしまいます。ラジオボタンのグループには、送信用のボタンが必要です。

空の `title` に言葉を入れても、各ボタンに名前は付きます。ほかに名前がないとき、ブラウザーは `title` を使うからです。これで4.1.2も満たします。ただし多くのブラウザーでは、ツールチップはマウスを顔の上に置いたときにしか出ないため、目の見える人でも、キーボードやタッチスクリーンで操作していれば言葉は見えません。コンポーネントがほかに何も受け付けない場合に限って使ってください。各ボタンに `aria-label` を付けても、隠したテキストと同じように名前が付きます。二つの違いは[名前がないアイコンだけのボタン](/ja/learn/icon-buttons)で比べています。どちらを使う場合も、付ける先はボタンです。`aria-hidden="true"` の付いた `svg` に `aria-label` を付けても無視されます。

`fieldset` は、入力欄だけでなくボタンもまとめられます。レイアウトの都合で使えない場合は、`role="group"` と、設問を指す `aria-labelledby` を持つ要素でも同じ役目を果たせます。顔の名前は、描いた順に悪いほうから良いほうへ付け、名前の並びを列の向きと揃えます。尺度の言葉はサイトが決めるものです。カードが「どのくらい役に立ちましたか？」と尋ねるなら、名前は両端の言葉に合わせて「役に立たなかった」から「とても役に立った」のようにします。

## 修正の確認

1. スクリーンリーダーを有効にし、Tabでカードに入ります。設問がグループの名前として読み上げられ、続いて各顔が値と「ボタン」で、「とても悪い」から「とても良い」まで、描かれた順に読み上げられます。
2. スクリーンリーダーの読み上げキーで、カードを先頭から読みます。最初に設問、続いて各顔とその値が読まれ、ボタンの後ろで言葉だけが単独に読まれることはありません。
3. 音声操作で「ふつうをクリック」と言うと、その顔が押されます。両端の顔と、中央のもう一つの顔でも試します。
4. スマートフォンでスクリーンリーダーを有効にし、各顔に触れます。その値が読み上げられ、ダブルタップで送信できます。カードは画面に収まり、すべての言葉が見えています。

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

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。CSSは最小限の例なので、顔の大きさ、顔の間隔、フォーカスの枠を実際のデザインで確認してください。このようなカードには、名前のない×印の閉じるボタンが付いていることもあります。それは[名前がないアイコンだけのボタン](/ja/learn/icon-buttons)で扱う問題です。回答後に顔と入れ替わるお礼の言葉が読み上げられるかは、[画面に出ても読み上げられないステータスメッセージ](/ja/learn/status-messages)で扱います。赤、黄、緑の丸のように、色だけで見分ける顔は別の問題で、色だけに頼る表示のトピックで扱います。そのガイドは準備中です。カード全体を、スクリーンリーダー、音声操作、スマートフォンで確認してください。

---

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