目次
- はじめに
- アクセシビリティとは?
- 見た目で分かることが、支援技術にも伝わるとは限らない
- ARIA属性とは?
aria-invalidの役割aria-describedbyの役割aria-invalidとaria-describedbyは役割が異なる- LaravelのBladeで実装する
- エラーがないときにARIA属性を付けない理由
- 複数の返信フォームでは
idを重複させない - 私が二度目に躓いたこと:文字列テストでは不十分だった
- DOM解析でHTMLの構造を確認する
- DOMXPathを使った確認例
- 通常時もテストする
- 複数フォームでは、エラー元だけを確認する
- 今回躓いたポイント
- アクセシビリティ対応は、特別な人だけのためではない
- アクセシビリティは後付けではなく、設計の一部
- 学んだこと
- おわりに
はじめに
Laravelで映画レビューアプリを開発している中で、レビュー投稿フォームと返信フォームのアクセシビリティを改善する機会がありました。
最初の私は、画面にエラーメッセージが表示されていれば十分だと思っていました。
「レビュー本文を入力してください」と赤い文字で表示されているのだから、どこが間違っているか分かるのでは?
しかし、それは画面を見て操作できる人を前提にした考え方でした。
画面を読み上げるスクリーンリーダーなどの支援技術には、エラーメッセージが表示されているだけでは、次の関係が正しく伝わらないことがあります。
- どの入力欄でエラーが発生したのか
- どのメッセージがその入力欄の説明なのか
- 入力欄が現在エラー状態なのか
そこで今回、aria-invalidとaria-describedbyというARIA属性を使って、入力欄の状態とエラーメッセージの関係をHTMLへ明示しました。
この記事では、映画レビューアプリでの実装を題材に、次の内容を初心者向けに整理します。
- アクセシビリティとは何か
- なぜアクセシビリティ対応が必要なのか
- ARIA属性とは何か
-
aria-invalidとaria-describedbyの役割 - LaravelのBladeでの実装方法
- Featureテストで躓いたポイント
- DOM解析によるテスト改善
CSSフレームワークや画面装飾は省き、HTML、Blade、テストの本質に絞って説明します。
アクセシビリティとは?
アクセシビリティ(Accessibility)とは、年齢、障害の有無、身体状況、利用端末、操作環境などにかかわらず、できるだけ多くの人がWebサイトやアプリを利用できるようにする考え方です。
例えば、Webアプリの利用者には次のような人がいます。
- スクリーンリーダーを使って画面の内容を音声で確認する人
- マウスを使わず、キーボードだけで操作する人
- 色の違いだけでは情報を判別しにくい人
- ケガなどにより一時的にマウスを使えない人
- 小さな画面や拡大表示を使っている人
- 周囲の環境や端末の都合で、普段とは異なる操作方法を使う人
アクセシビリティは、一部の人だけに向けた特別な機能ではありません。
利用者によって異なる操作方法や情報の受け取り方を考慮し、誰にとっても理解しやすく、操作しやすいWebアプリを作るための基本的な設計です。
私は、誰もが映画の感想を投稿し、他の人のレビューを楽しめる、やさしいWebアプリを作りたいと考えています。
そのためには、画面を見れば分かる実装だけでなく、支援技術を通して利用する人にも意味が伝わるHTMLを書く必要があります。
見た目で分かることが、支援技術にも伝わるとは限らない
レビュー投稿フォームで入力エラーが発生したとします。
画面には次のメッセージが表示されています。
レビュー本文を入力してください。
画面を見ている人は、入力欄の近くに赤い文字が表示されていれば、レビュー本文に問題があると推測できます。
しかし、HTML上で入力欄とエラーメッセージの関係が明示されていない場合、スクリーンリーダーなどの支援技術には、その関係が正しく伝わらないことがあります。
利用者にはエラーメッセージだけが読み上げられても、次のことが分からない可能性があります。
- どの入力欄を修正すればよいのか
- 現在フォーカスしている入力欄がエラーなのか
- 読み上げられた文章が入力欄の説明なのか
つまり、
画面では分かるけれど、HTMLの構造としては関係が伝わっていない
という状態です。
アクセシブルなフォームを作るには、見た目だけで表現している情報を、HTMLにも意味として持たせる必要があります。
ARIA属性とは?
aria-invalidとaria-describedbyは、どちらもHTML要素へ追加する属性です。
その中でも、ARIAの仕様で定義されているため、一般にARIA属性と呼ばれます。
ARIAは、Accessible Rich Internet Applicationsの略です。
ARIA属性を使うと、画面の見た目だけでは分からない状態や関係を、スクリーンリーダーなどの支援技術へ伝えられます。
例えば、次のtextareaには、入力欄であること以外の情報がありません。
<textarea id="body"></textarea>
ここへaria-invalidを追加すると、入力欄がエラー状態であることを表せます。
<textarea
id="body"
aria-invalid="true">
</textarea>
さらにaria-describedbyを追加すると、この入力欄を説明する要素を指定できます。
<textarea
id="body"
aria-invalid="true"
aria-describedby="review-body-error">
</textarea>
<p id="review-body-error">
レビュー本文を入力してください。
</p>
この例を分解すると、次のようになります。
| 種類 | 名前 | 役割 |
|---|---|---|
| HTML要素 | textarea |
レビュー本文を入力する欄 |
| HTML属性 | id |
要素を識別する名前 |
| ARIA属性 | aria-invalid |
入力欄がエラー状態であることを伝える |
| ARIA属性 | aria-describedby |
入力欄を説明する要素のidを指定する |
| HTML要素 | p |
エラーメッセージを表示する |
| HTML属性 | id |
aria-describedbyから参照される名前 |
ARIA属性は画面の色やレイアウトを変更するものではありません。
HTMLへ状態や関係の意味を追加し、支援技術が内容を理解できるようにするための属性です。
aria-invalidの役割
aria-invalidは、入力値が現在エラー状態であることを支援技術へ伝えるARIA属性です。
<textarea aria-invalid="true"></textarea>
aria-invalid="true"が付いていると、支援技術はその入力欄をエラー状態として扱えます。
今回の実装では、常に付けるのではなく、バリデーションエラーが発生したときだけ追加します。
通常時は次の状態です。
<textarea id="body"></textarea>
エラー発生時は次の状態になります。
<textarea
id="body"
aria-invalid="true">
</textarea>
この違いにより、現在の入力欄が正常なのか、修正が必要なのかを支援技術へ伝えられます。
aria-describedbyの役割
aria-describedbyは、その要素を説明する別の要素をidで指定するARIA属性です。
<textarea
aria-describedby="review-body-error">
</textarea>
<p id="review-body-error">
レビュー本文を入力してください。
</p>
aria-describedbyの値と、説明要素のidを同じにすることで、入力欄と説明文を関連付けます。
aria-describedby="review-body-error"
↓
id="review-body-error"
私はこの関係を理解するとき、次のように考えると分かりやすくなりました。
- エラーメッセージへ名前を付ける
<p id="review-body-error">
- 入力欄から、その名前を参照する
<textarea aria-describedby="review-body-error">
つまり、
この入力欄の説明は、
review-body-errorという要素です
とHTML上で伝えています。
aria-invalidとaria-describedbyは役割が異なる
最初は、2つともエラーを表すための属性なので、違いが分かりにくく感じました。
役割を整理すると次のようになります。
| ARIA属性 | 伝える内容 |
|---|---|
aria-invalid="true" |
この入力欄はエラー状態である |
aria-describedby="..." |
この入力欄の説明は、指定した要素である |
aria-invalidだけでは、何が問題なのかまでは伝えられません。
aria-describedbyだけでは、入力欄が現在エラー状態であることまでは明示できません。
そのため、バリデーションエラーを伝える場面では、2つを組み合わせて使用します。
<textarea
aria-invalid="true"
aria-describedby="review-body-error">
</textarea>
<p id="review-body-error">
レビュー本文を入力してください。
</p>
LaravelのBladeで実装する
ここからは、LaravelのBladeでバリデーションエラーが発生した場合だけARIA属性を追加する例を見ていきます。
バリデーション対応前
最初のフォームは、次のような構造でした。
<label for="body">レビュー本文</label>
<textarea
id="body"
name="body">{{ old('body') }}</textarea>
@error('body')
<p>{{ $message }}</p>
@enderror
この状態でも、画面にはエラーメッセージが表示されます。
しかし、textarea自身がエラー状態であることや、どのエラーメッセージと関連しているかはHTML上で示されていません。
バリデーション対応後
<label for="body">レビュー本文</label>
<textarea
id="body"
name="body"
@error('body')
aria-invalid="true"
aria-describedby="review-body-error"
@enderror
>{{ old('body') }}</textarea>
@error('body')
<p id="review-body-error">
{{ $message }}
</p>
@enderror
バリデーションエラーが発生すると、次のARIA属性がtextareaへ追加されます。
aria-invalid="true"
aria-describedby="review-body-error"
また、エラーメッセージには対応するidを付けています。
id="review-body-error"
これにより、入力欄の状態とエラーメッセージの関係がHTML上で明確になります。
エラーがないときにARIA属性を付けない理由
通常時までaria-invalid="true"を付けてしまうと、入力内容に問題がないにもかかわらず、支援技術へエラー状態として伝わってしまいます。
そのため、今回の実装ではBladeの@errorを使い、エラーがある場合だけARIA属性を出力しています。
通常時のHTMLは次のとおりです。
<textarea
id="body"
name="body">
</textarea>
エラー時のHTMLは次のとおりです。
<textarea
id="body"
name="body"
aria-invalid="true"
aria-describedby="review-body-error">
</textarea>
アクセシビリティ対応では、属性を追加することだけでなく、現在の状態を正しく表すことが重要です。
複数の返信フォームではidを重複させない
映画レビューアプリの作品詳細画面には、レビューごとに返信フォームがあります。
複数のフォームが同じ画面へ表示されるため、すべてのエラーメッセージへ同じidを付けることはできません。
HTMLのidは、同じページ内で一意にする必要があります。
そこで、レビューIDを使ってエラーメッセージごとに異なるidを作ります。
<textarea
name="body"
aria-invalid="true"
aria-describedby="comment-body-error-{{ $review->id }}">
</textarea>
<p id="comment-body-error-{{ $review->id }}">
{{ $message }}
</p>
例えばレビューIDが10なら、次のHTMLになります。
<textarea
aria-invalid="true"
aria-describedby="comment-body-error-10">
</textarea>
<p id="comment-body-error-10">
返信本文を入力してください。
</p>
レビューIDが11なら、別のidになります。
<textarea
aria-invalid="true"
aria-describedby="comment-body-error-11">
</textarea>
<p id="comment-body-error-11">
返信本文を入力してください。
</p>
これにより、それぞれの返信フォームとエラーメッセージを正しく関連付けられます。
私が二度目に躓いたこと:文字列テストでは不十分だった
ARIA属性を実装したあと、Featureテストも追加しました。
最初はassertSeeInOrder()を使い、次の文字列が順番どおりに出力されていることを確認していました。
$response->assertSeeInOrder([
'aria-invalid="true"',
'aria-describedby="review-body-error"',
'id="review-body-error"',
], false);
しかし、このテストには問題がありました。
assertSeeInOrder()が確認しているのは、レスポンス内に文字列が指定した順番で存在することです。
ARIA属性が、本当に対象のtextareaへ付いているかまでは確認していません。
例えば、誤って次のHTMLが出力されていたとします。
<textarea id="body"></textarea>
<div
aria-invalid="true"
aria-describedby="review-body-error">
</div>
<p id="review-body-error">
レビュー本文を入力してください。
</p>
ARIA属性はtextareaではなく、無関係なdivへ付いています。
それでも文字列の順番だけを見るテストでは、通ってしまう可能性があります。
つまり、
ARIA属性が画面内に存在すること
は確認できても、
対象の
textarea自身にARIA属性が付いていること
は保証できていませんでした。
DOM解析でHTMLの構造を確認する
この問題を解決するため、FeatureテストではDOMDocumentとDOMXPathを使ってHTMLをDOMとして解析しました。
DOM解析を使うことで、文字列ではなくHTML要素そのものを取得して確認できます。
確認した内容は次のとおりです。
- 対象の
textareaが1件存在する - 対象の
textarea自身にaria-invalid="true"が付いている - 対象の
textarea自身にaria-describedbyが付いている -
aria-describedbyの参照先となる要素が存在する - 通常時の
textareaにはARIA属性が付いていない - 複数の返信フォームでは、エラーが発生したフォームだけにARIA属性が付いている
DOMXPathを使った確認例
次は考え方を伝えるための簡略化した例です。
$html = $response->getContent();
$document = new DOMDocument();
libxml_use_internal_errors(true);
$document->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($document);
$textarea = $xpath
->query('//*[@id="body"]')
->item(0);
$this->assertInstanceOf(DOMElement::class, $textarea);
$this->assertSame(
'true',
$textarea->getAttribute('aria-invalid')
);
$this->assertSame(
'review-body-error',
$textarea->getAttribute('aria-describedby')
);
このテストでは、id="body"を持つ要素を取得し、その要素自身に設定されている属性を確認しています。
文字列の存在だけではなく、HTMLの構造として正しく実装されていることを検証できます。
通常時もテストする
エラー時だけを確認するのではなく、通常時に不要なARIA属性が付いていないことも確認します。
$this->assertFalse(
$textarea->hasAttribute('aria-invalid')
);
$this->assertFalse(
$textarea->hasAttribute('aria-describedby')
);
アクセシビリティ対応では、属性が存在することだけでなく、正しい状態のときだけ存在することも重要です。
エラーがない入力欄へaria-invalid="true"が付いていれば、支援技術へ誤った状態を伝えてしまいます。
複数フォームでは、エラー元だけを確認する
返信フォームのテストでは、同じ画面内に複数の返信フォームを用意しました。
そのうえで、バリデーションエラーを発生させたフォームだけにARIA属性が付いていることを確認しました。
例えば、レビューID10の返信フォームでエラーが発生した場合は、次の状態を期待します。
<textarea
aria-invalid="true"
aria-describedby="comment-body-error-10">
</textarea>
一方、レビューID11の返信フォームにはARIA属性が付いていないことを確認します。
このテストにより、別の返信フォームへ誤ってエラー表示やARIA属性が付いていないことも検証できます。
今回躓いたポイント
今回の実装では、主に次の点で躓きました。
1. 画面にエラーが表示されれば十分だと思っていた
見た目で分かることと、支援技術へ意味が伝わることは別でした。
エラーの色や配置だけでは、入力欄とエラーメッセージの関係がHTML上で表現されていません。
2. aria-invalidとaria-describedbyの違いが分からなかった
最初はどちらもエラーを伝える属性に見えました。
整理すると、役割は次のように異なります。
-
aria-invalidは入力欄の状態を伝える -
aria-describedbyは入力欄と説明文の関係を伝える
3. aria-describedbyの値には何を書くのか迷った
aria-describedbyの値には、説明として参照したい要素のidを書きます。
aria-describedby="review-body-error"
そして、エラーメッセージ側へ同じidを付けます。
id="review-body-error"
4. 複数フォームで同じidを使えないことに注意が必要だった
レビューごとに返信フォームがあるため、固定のidでは重複してしまいます。
レビューIDを含めることで、ページ内で一意のidを作りました。
5. 文字列の順番だけでは実装の正しさを保証できなかった
assertSeeInOrder()では、ARIA属性が対象要素自身へ付いているかを確認できませんでした。
DOM解析へ変更することで、要素単位で属性を検証できるようになりました。
6. エラー時だけでなく通常時も確認する必要があった
ARIA属性は多ければよいわけではありません。
現在の状態を正しく表す必要があるため、正常時に不要な属性が付いていないこともテストしました。
アクセシビリティ対応は、特別な人だけのためではない
アクセシビリティについて学ぶ前は、障害のある方のために追加する特別な対応という印象がありました。
しかし、実際にはもっと広い考え方です。
人はいつでも同じ環境、同じ身体状況、同じ操作方法でWebアプリを使うとは限りません。
- ケガで一時的にマウスを使えない
- 画面が見づらく、拡大表示を使っている
- キーボード操作の方が使いやすい
- 音声読み上げを利用している
- 小さな画面で操作している
アクセシビリティを意識した設計は、様々な利用者や利用状況に対応しやすいWebアプリにつながります。
また、HTMLへ正しい意味や関係を持たせることは、スクリーンリーダーだけでなく、ブラウザ、検証ツール、テストコード、将来保守する開発者にとっても理解しやすい実装になります。
アクセシビリティは後付けではなく、設計の一部
アクセシビリティ対応というと、完成後に追加する調整のように感じるかもしれません。
しかし、入力欄、ラベル、エラーメッセージ、フォーカス、キーボード操作などは、画面を設計する段階から考えた方が自然です。
今回のようにフォームを実装するときも、最初から次の点を意識できます。
- 入力欄に対応する
labelがあるか - エラー状態を支援技術へ伝えられるか
- エラーメッセージと入力欄が関連付いているか
- キーボードだけでも操作できるか
- 色だけに依存して情報を伝えていないか
アクセシビリティを品質要件の一つとして扱うことで、後から大きく修正する必要も減らせます。
学んだこと
今回の実装を通して、アクセシビリティ対応はARIA属性を追加するだけの作業ではないと学びました。
重要なのは、
画面を見たときに分かる情報を、異なる方法で利用する人にも正しく伝えること
です。
また、実装だけでなく、テストでもHTMLの構造を正しく検証する必要がありました。
今回学んだ内容を整理すると、次のようになります。
- 見た目だけでなくHTMLにも意味を持たせる
- 入力欄の状態は
aria-invalidで伝える - 入力欄と説明文の関係は
aria-describedbyで伝える -
aria-describedbyから参照するidはページ内で一意にする - エラー時だけ必要なARIA属性を出力する
- Featureテストでは対象要素自身の属性を確認する
- 複数フォームではエラー元のフォームだけを検証する
- 通常時に不要なARIA属性がないことも確認する
おわりに
私は、誰もが映画の感想を投稿し、他の人のレビューを楽しめる、やさしいWebアプリを作りたいと思っています。
アクセシビリティは、画面を華やかにする機能ではありません。
対応しても、見た目が大きく変わらないこともあります。
それでも、その小さな実装によって、今まで操作しづらかった人が利用できるようになる可能性があります。
「画面に表示されているから分かるはず」ではなく、
この情報は、異なる方法でWebアプリを利用する人にも正しく伝わっているだろうか
と考えることが、アクセシブルな開発の第一歩だと感じました。
Laravelを学び始めた方にも、バリデーションやフォームを実装する段階から、アクセシビリティを意識してほしいと思います。
私自身も、今回のaria-invalidとaria-describedbyだけで終わらせず、キーボード操作、フォーカス管理、見出し構造、代替テキストなど、誰もが使いやすいWebアプリを作るための知識を少しずつ学んでいきます。