はじめに
aria-liveを使って、フォームのエラー内容を実装したところ、スクリーンリーダーに読み上げられない実装をしてしまったので、そのやらかしを記事にしました。
aria-liveとは
コンテンツが最初に読み込まれた後に変更されると、支援技術 (AT) ユーザーはその変更を「見る」ことができない場合があります。変更の中には重要なものもあります。また、重要でないものもあります。 aria-live 属性は、ユーザーに更新情報を通知し、重要性と緊急性に基づいて、AT ユーザーにコンテンツの変更を即座に通知するか、積極的に通知するか、受動的な通知をするかを開発者が選ぶことができます。
mdnより引用
今回ではフォームのテキスト入力でバリデーションエラーが出た場合、スクリーンリーダーに通知して読み上げられるまでが実装のゴールでした。
実際にどんなコード書いたのか
// 失敗したコード
{isError &&(
<p aria-live="polite">エラーです</p>
)}
アクセシビリティの実装を普段している人から見たらひと目で分かると思います。
エラーが出たらaria-liveが付与されたpタグが表示されるというコードです。
エラーが出るまでは、aria-liveが付与されたpタグは存在していません。
フリー株式会社のアクセシビリティガイドラインに 「注意:ページのロード時にARIAライブ・リージョンが存在しないと読み上げられない」 と書いてあるように最初から存在していないと読み上げられないのです。
以下が正しく動作するコードです。
// 正しいコード
<p aria-live="polite">
{isError && "エラーです"}
</p>
Demo
実際にスクリーンリーダーに反応するかどうかデモを実装しました。
StackBlitzにアップロードしてあります。
import { useState } from 'react';
import type { ChangeEvent } from 'react';
import { z } from 'zod';
import './App.css';
const inputSchema = z
.string()
.min(5, '5文字以上入力してください')
.max(10, '10文字以内で入力してください');
function App() {
// 良い例用のstate
const [goodInputValue, setGoodInputValue] = useState<string>('');
const [goodError, setGoodError] = useState<string>('');
// 悪い例用のstate
const [badInputValue, setBadInputValue] = useState<string>('');
const [badError, setBadError] = useState<string>('');
// 良い例のハンドラー
const handleGoodChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setGoodInputValue(value);
const result = inputSchema.safeParse(value);
if (!result.success) {
setGoodError(result.error.issues[0].message);
} else {
setGoodError('');
}
};
// 悪い例のハンドラー
const handleBadChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setBadInputValue(value);
const result = inputSchema.safeParse(value);
if (!result.success) {
setBadError(result.error.issues[0].message);
} else {
setBadError('');
}
};
return (
<>
<h1>aria-live="polite" の使い方</h1>
{/* 良い例 */}
<section className="example good">
<h2>✓ 良い例</h2>
<p className="description">
aria-live領域を常にDOMに配置し、中身だけを切り替える
</p>
<input
type="text"
value={goodInputValue}
onChange={handleGoodChange}
placeholder="5文字以上10文字以内で入力"
className="input-field"
/>
<div aria-live="polite" className="live-region">
{goodError && <span className="error-message">{goodError}</span>}
</div>
</section>
<section className="example bad">
<h2>✗ 悪い例</h2>
<p className="description">
aria-live要素自体を条件付きレンダリング(スクリーンリーダーが変更を検知できない)
</p>
<input
type="text"
value={badInputValue}
onChange={handleBadChange}
placeholder="5文字以上10文字以内で入力"
className="input-field"
/>
{badError && (
<div aria-live="polite">
<span className="error-message">{badError}</span>
</div>
)}
</section>
</>
);
}
export default App;
補足
SSR(サーバーサイドレンダリング)だとaria-liveは機能しないので、idとaria-describedbyでエラー文章を紐づけてaria-invalid="true"を返す方が良いようです。
<input
type="text"
aria-describedby="errorId"
aria-invalid={isError ? true : false}
/>
{isError && (
<p id="errorId">
エラーです
</p>
)}
参考