# 見えているラベルで読み上げられないリッチテキストエディター

Source: https://easeweb.dev/learn/rich-text-editors
Topics: カスタム入力 > リッチテキストエディターが、横に見えるラベルの名前で入力欄として読み上げられる (WCAG 4.1.2, 1.3.1, 2.5.3)
The fix: エディターの編集できる要素にrole="textbox"とaria-multiline="true"を付け、横に表示されたラベルで名前を付けます。aria-labelledbyでそのラベルを指すか、エディターのライブラリーのラベル設定に同じ言葉を入れてaria-labelにします。
Test with: keyboard, screen reader, speech input
References: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F59 https://www.w3.org/WAI/WCAG22/Techniques/failures/F68 https://www.w3.org/WAI/WCAG22/Techniques/failures/F111 https://html.spec.whatwg.org/multipage/forms.html#category-label https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/ https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/textbox_role https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html https://prosemirror.net/docs/ref/#view.EditorProps.attributes https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-ui/src/editableui/inline/inlineeditableuiview.ts https://github.com/facebook/lexical/blob/main/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx

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

## 場面

あるショップのお問い合わせフォームに、「メッセージ」と見出しの付いた欄があります。上には太字・斜体・箇条書きのボタンが並び、書いた文章に書式を付けられます。画面では、フォームのほかの欄と同じようにラベルが付いて見えます。ところが、スクリーンリーダーでTabキーを押してこの欄に入っても、「メッセージ」とは読み上げられません。一つ目の例では、欄に名前がなく、入力欄であることも伝わりません。Chromeのアクセシビリティツリーでは、ただのgeneric要素です。二つ目の例では、エディターのライブラリーが欄に名前を付けています。名前は「Rich Text Editor. Editing area: main」で、ライブラリーが作るどのエディターにも同じ名前が付くため、エディターが二つあるフォームでは、同じ名前の欄が二つ並びます。音声操作の利用者が画面の言葉どおり「メッセージをクリック」と話しても、どちらの例でも何も起きません。

## 確認できる問題

`textarea`にはプレーンテキストしか入らないため、リッチテキストエディターには使えません。太字や箇条書きを表示するために、エディターは`contenteditable`で`div`を編集できるようにします。この`div`はフォームの入力欄ではありません。`label`要素が名前を付けられるのは、`button`、`input`、`meter`、`output`、`progress`、`select`、`textarea`など、HTMLでラベル付け可能とされる要素だけです。そのため、`div`を指す`for="message"`は何の名前にもならず、ラベルをクリックしても何も起きません。画面で見えるラベルと欄の結び付きがコードにないので、1.3.1を満たしません。また`contenteditable`は`div`を編集できるようにするだけで、ロールは与えません。スクリーンリーダーには入力欄であることも、何の欄かも伝わらず、ロールと名前の両方で4.1.2を満たしません（W3Cの失敗例F59とF68）。W3Cの失敗例F111は、見えるラベルがあるのに名前がないコントロールというこの場面全体を取り上げ、1.3.1、2.5.3、4.1.2のすべてを満たさないとしています。`role="textbox"`を足すだけでは、ロールは直っても名前は空のままです。

CKEditor 5やLexicalのように、ロールを自分で付けるエディターのライブラリーもあります。CKEditor 5は、名前が指定されていなければ`aria-label`も付けます。「Rich Text Editor. Editing area: main」の「main」は編集領域の名前で、ページ上のどこにもない言葉です。欄にロールと名前は付きますが、その名前は上に表示されたラベルではありません。2.5.3は、見えている言葉が名前に含まれ、その言葉を話せば操作できることを求めていますが、名前には「メッセージ」がどこにもありません。画面のラベルは欄と結び付いていないままなので、1.3.1も満たしません。

## 利用者への影響

どの例も見た目は同じで、マウスを使う目の見える人には問題が見えません。ライブラリーが付ける名前の問題は、エディターが増えるほど大きくなります。商品の「説明」と「お手入れ方法」の二つのエディターがあるフォームでは、同じ名前の欄が二つになり、区別できるのは並び順だけです。

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

axeの`aria-input-field-name`ルールは、テキストボックスのロールを持つ要素を調べるため、`role="textbox"`があって名前のない欄は検出します。ロールのない`div`はaxeにとって入力欄ではなく、どのルールも報告しません。ライブラリーの名前も名前には違いないため、二つ目の例も違反としては報告されません。この例のようにラベルの`for`が欄を指したままなら、ラベルの言葉が名前にないため、axeは確認が必要な項目に挙げるだけです。EaseWebの診断は、ロールのない編集可能な`div`を4.1.2の問題として報告し、「Rich Text Editor…」や「Editor editing area…」という名前のエディターは、人による確認に回します。表示されたラベルを名前にすべきかは、人が見て判断することだからです。iframeの中のエディターには届きません。手作業でもAIアシスタントでも、手早い修正では`div`に`id`を付けてラベルの`for`と合わせることがあります。レビューでは正しく見えますが、何の名前にもなりません。

## 修正前：ロールのない編集可能なdiv

```html
<label for="message">メッセージ</label>
<div class="toolbar" role="toolbar" aria-label="書式">
  …
</div>
<div id="message" class="editor" contenteditable="true">
  <p>届いた荷物のふたが割れていました。</p>
</div>
```

ラベルの`for`は`div`の`id`と一致していますが、`div`はラベル付け可能な要素ではないため、ラベルは何の名前にもなりません。`div`にはロールもありません。

## 修正前：エディターのライブラリーが付けた名前

```html
<label for="message">メッセージ</label>
<div class="toolbar" role="toolbar" aria-label="書式">
  …
</div>
<div id="message" class="editor" contenteditable="true" role="textbox" aria-label="Rich Text Editor. Editing area: main">
  <p>届いた荷物のふたが割れていました。</p>
</div>
```

ライブラリーが欄をテキストボックスにし、名前も付けていますが、ライブラリー自身の言葉です。「メッセージ」は画面にあるだけです。

## 修正後：ラベルを指すテキストボックス

```html
<div id="message-label">メッセージ</div>
<div class="toolbar" role="toolbar" aria-label="書式">
  …
</div>
<div id="message" class="editor" contenteditable="true" role="textbox" aria-multiline="true" aria-labelledby="message-label">
  <p>届いた荷物のふたが割れていました。</p>
</div>
```

`role="textbox"`と`aria-multiline="true"`で欄の種類を伝え、`aria-labelledby`でラベルの文字そのものを名前にします。スクリーンリーダーはロールと一緒に「メッセージ」と読み、音声操作は「メッセージをクリック」に反応します。`label`要素では`div`に名前を付けられないため、ラベルはただのテキストにしています。ライブラリーの`aria-label`は削除しました。残しても`aria-labelledby`が優先されますが、次にコードを読む人には、どちらの名前が使われるのか分かりません。

## 修正後：エディターの設定にラベルの言葉を書き写す

```html
<div>メッセージ</div>
<div class="toolbar" role="toolbar" aria-label="書式">
  …
</div>
<div id="message" class="editor" contenteditable="true" role="textbox" aria-multiline="true" aria-label="メッセージ">
  <p>届いた荷物のふたが割れていました。</p>
</div>
```

多くのエディターのライブラリーは、入力部分の名前を設定で受け取ります。CKEditor 5にはラベルの設定があり、Tiptapの土台であるProseMirrorは、`attributes`オプションで入力部分に任意の属性を付けられます。そこにラベルと同じ言葉を入れ、ライブラリーが付けない場合は`role`と`aria-multiline`も同じ方法で付けると、上のHTMLになります。欄は「メッセージ」という名前のテキストボックスになり、スクリーンリーダーにも音声操作にも画面と同じ言葉が伝わります。ただし、これが成り立つのは、ラベルと設定の言葉がそろっている間だけです。ラベルと設定は別のファイルにあることが多く、設定を変えずにラベルの文言を変えたり翻訳したりすると、名前には古い言葉が残り、音声操作は新しい言葉に反応しなくなります。

## どの方法を選ぶか

どちらも欄にロールを与え、ラベルの言葉を名前にします。`aria-labelledby`でラベルを指してください。言葉の元が一つなので、ラベルが変われば名前も必ず一緒に変わります。書き写した`aria-label`ではそうなりません。書き写すのは、入力部分に設定できるのがライブラリーのラベル設定だけの場合に限ります。そのときも、ラベルの表示と同じ一つの変数から設定に渡して、二つがずれないようにします。

## 実装の判断

**ライブラリーで設定し、ページで確かめます。** 属性を付けるのは、編集の起点となる要素です。この要素はライブラリーが作るため、ライブラリーのオプションで設定します。渡した要素に付けた属性が入力部分に届くのは、CKEditor 5が`aria-label`を引き継ぐように、ライブラリーがそれを残す場合だけです。バージョンを上げたら、そのたびにブラウザーで結果を確認してください。ライブラリーによっては独自の`role`や`aria-label`を付け、その既定値がバージョンで変わると、エディターの名前も一緒に変わります。

**複数行。** `aria-multiline="true"`は、Enterキーでフォームを送信するのではなく改行することを伝えます。Chromeは編集可能なテキストボックスを自分で複数行として伝えますが、この属性を付ければ、ブラウザーの推測に任せずマークアップで示せます。

**ラベルのクリック。** `label`要素はクリックすると欄にフォーカスを移しますが、エディターの名前になっている`div`は移しません。デザイン上クリックで入れる必要があれば、エディターの`focus()`を呼ぶ処理を足します。達成基準を満たすためには必須ではありません。

**ヒント、必須、プレースホルダー。** 「太字と箇条書きは保存されます」のようなヒントは独立した要素に書き、`aria-describedby`で指します。`div`には`required`を付けられないため、見た目の表示と合わせて、テキストボックスに`aria-required="true"`を付けます。エディターがCSSで描く灰色のプレースホルダー（`data-placeholder`属性から描くことが多い）は名前ではなく、入力すると消えます。

**書式が要らない場合。** 配達員への短い伝言のように、太字も箇条書きもリンクも要らない文章なら、`label`要素を付けた`textarea`で十分で、ここに書いた対応はどれも要りません。

**iframeの中のエディター。** iframeの中に描かれるエディターには、名前を付けるものが二つあります。`title`を持つフレームと、その中の編集可能なページです。アクセシビリティツリーで両方を確認してください。診断は後者には届きません。

## 修正の確認

1. ブラウザーのアクセシビリティツリーで各エディターの編集できる要素を調べ、ロールが複数行のテキストボックスで、名前が横に表示されたラベルと同じ言葉で始まることを確認する。
2. スクリーンリーダーでTabキーを使って各エディターに入り、見えるラベルがロールと一緒に読み上げられること、同じページの二つのエディターが別の名前で読まれることを確認する。
3. 音声操作で「クリック」と各エディターの見えるラベルを言い、そのエディターにフォーカスが移ることを確認する。
4. コードでラベルの文言を一つ変えるか、ページの言語を切り替えてから手順1を繰り返し、名前も一緒に変わったことを確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。エディターのライブラリーを更新した際は、再度確認します。

## 制限

このサンプルは、どのエディターでも同じ結果になることを保証するものではありません。扱うのは入力部分のロールと名前で、エディターのほかの部分は扱いません。インデントのためにTabキーを取り込み、キーボードで抜け出せなくなるエディターは、[フォーカスが抜け出せないエディターや埋め込み部品](/ja/learn/keyboard-traps)で扱っています。オンかどうかを伝えない太字ボタンは[押された状態が伝わらないトグルボタン](/ja/learn/toggle-buttons)で、名前のないツールバーのアイコンは[名前がないアイコンだけのボタン](/ja/learn/icon-buttons)で扱っています。ツールバーを一つのTab位置にまとめ、矢印キーで移動できるようにすることは、「ツールバーを矢印キーで操作でき、Tabの移動は1回で済む」のトピックに属しますが、ガイドはまだありません。日本語の変換を確定するEnterキーでエディターが反応してしまう問題は、[IMEの変換確定のEnterで送信される入力欄](/ja/learn/ime-composition)で扱っています。エディターに画像を入れるときに代替テキストを尋ねることは[代替テキストを尋ねない画像のアップロード](/ja/learn/alt-text-prompts)で、HTML標準の入力欄のラベルは[ラベルがない・表示と食い違うフォームの入力欄](/ja/learn/form-labels)で扱っています。実際に使用する支援技術で、操作全体を確認してください。

---

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