こんにちは、ENECHANGEでエンジニアをしている清水です。
今回は、AIエージェントと協働して、アクセシビリティ改善がどこまで実現できるのか、その可能性について検証してみました。
具体的には、Cursorを使ってアクセシビリティに関する問題点の洗い出しや、コードの修正を依頼してみました。
なぜAIエージェントでアクセシビリティ改善に挑戦したのか
アクセシビリティ対応は範囲が広く、知識が属人化しやすい分野だと感じています。
プロジェクトやチーム内でガイドラインやチェックリストを設ければ、一定の品質は担保できますが、そもそも導入自体がされていないケースも珍しくありません。
そうした中で、AIエージェントを活用し、開発プロセスの中に自然にアクセシビリティ対応を組み込める仕組みが作れれば、属人性を減らしつつ効率的な品質担保ができるのではないかと考えました。
試したこと
Cursorとともに、アクセシビリティ改善がどこまで実現できるのかを、以下の2つの観点から検証しました。
- APG(Authoring Practices Guide)ガイドラインをもとに既存コードをレビュー
- レビュー結果をもとに、改善提案を受ける
1. ガイドラインをもとに既存コードをレビュー
まず、既存の実装に対して「どこがアクセシビリティ的に不十分なのか」をCursorにレビューさせるアプローチを試しました。
今回は、WAI-ARIA Authoring Practices Guide(APG)を Cursor の Docs 機能に読み込ませた上で、以下のコードを対象にレビューを依頼しました。
使用したコードとその問題点
const RadioGroup = () => { const [selectedValue, setSelectedValue] = useState<string>(""); const options = [ { value: "s", label: "Sサイズ" }, { value: "m", label: "Mサイズ" }, { value: "l", label: "Lサイズ" }, ]; const handleChange = (value: string) => { setSelectedValue(value); }; return ( // ❌ role="radiogroup" が設定されていない // スクリーンリーダーがこのグループ全体を「ラジオボタングループ」として認識できない <div className={styles.group}> {options.map((option) => ( // ❌ input 要素に関連付けられた <label> が存在しない。 // スクリーンリーダーは、ラジオボタンのラベルを読み上げることができない。 // ラベルをクリックしても、ラジオボタンが選択されない。 <div key={option.value} className={styles.option}> <input type="radio" name="sample-radio" value={option.value} checked={selectedValue === option.value} onChange={() => handleChange(option.value)} className={styles.input} /> <span className={styles.label}>{option.label}</span> </div> ))} </div> ); };
使用したプロンプト

レビュー結果
Cursorは、APGのガイドラインに基づいて、以下の問題点を指摘してくれました。
1. aria-activedescendant の欠如
- キーボード操作でどのオプションがアクティブ状態かを示せない
2. role="radiogroup" が未設定
- スクリーンリーダーがこのブロックを「ラジオグループ」として認識できない
3. 各選択肢に role="radio" と aria-checked がない
- 選択肢が何なのか、どれが選ばれているのかがスクリーンリーダーに伝わらない
4. キーボードナビゲーション未対応
- 矢印キーでの移動、Enter/Spaceでの選択ができない
5. 構造的なHTML要素が不適切
<ul>と<li>ではなく<div>で実装されている
※ APGのExampleでは、ネイティブの<input type="radio">では難しいキーボード操作やカスタムスタイルなどに対応するため、<ul>と<li>などのHTML要素とARIA属性を組み合わせて使うことが前提とされています。
www.w3.org
コントラスト比の計算について
「APGのRadioGroupパターンに準拠しているかどうか」という指示に対しては、構造的な問題などを指摘してくれましたが、コントラスト比などの視覚的な観点については指摘されませんでした。
そこで、コントラスト比の確認に観点を絞って指示した場合はどうなるか、追加で検証してみました。
検証結果
問題自体は指摘されたものの、AIが返した数値と実際のコントラスト比にズレがあり、計算精度に課題があることが分かりました。
※ 正確な数値は WebAIMを使用して確認しています。
- 選択状態のラベルのコントラスト比(#888 on #f9f9f9)
- AIの回答: 3.8:1
- 実際の数値: 4.48:1
- 改善案として提案されたコントラスト比( #000 on #f9f9f9 )
- AIの回答: 21:1
- 実際の数値: 19.94:1
| 指摘内容 | 提案内容 |
|---|---|
![]() |
![]() |
そのため、計算が求められる領域では、AIの出力を鵜呑みにせず、別のアプローチを検討することが重要だと感じました。
2 レビュー結果をもとに、改善提案を受ける
続いて、レビューで挙がった問題点をもとに、Cursorに修正を依頼したところ、以下のようなコードを提案してくれました。
改善されたポイント
- キーボードナビゲーション(矢印キー、Enter/Space)を実装
role="radiogroup"とaria-labelでグループ全体を定義aria-activedescendantでキーボードフォーカス位置を管理<ul><li>構造でセマンティックなHTMLに変更- 各選択肢に
role="radio"とaria-checkedを追加 <label>要素でinputとラベルを適切に関連付け
提案されたコード
const RadioGroup = () => { const [selectedValue, setSelectedValue] = useState<string>(""); const [focusedIndex, setFocusedIndex] = useState<number>(-1); const options = [ { value: "s", label: "Sサイズ" }, { value: "m", label: "Mサイズ" }, { value: "l", label: "Lサイズ" }, ]; const handleChange = (value: string) => { setSelectedValue(value); }; // ✅ キーボードナビゲーション(矢印キー、Enter/Space)を実装 const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case "ArrowDown": e.preventDefault(); setFocusedIndex((prev) => (prev + 1) % options.length); break; case "ArrowUp": e.preventDefault(); setFocusedIndex((prev) => (prev - 1 + options.length) % options.length); break; case "Enter": case " ": e.preventDefault(); if (focusedIndex >= 0) { handleChange(options[focusedIndex].value); } break; } }; return ( // ✅ role="radiogroup"とaria-labelでグループ全体を定義 <div role="radiogroup" aria-label="サイズ選択" // ✅ aria-activedescendantでキーボードフォーカス位置を管理 aria-activedescendant={focusedIndex >= 0 ? `option-${options[focusedIndex].value}` : undefined} onKeyDown={handleKeyDown} onBlur={() => setFocusedIndex(-1)} > {/* ✅ <ul><li>構造でセマンティックなHTMLに変更 */} <ul className={styles.group}> {options.map((option, index) => ( <li key={option.value} className={styles.option}> <input type="radio" name="sample-radio" value={option.value} checked={selectedValue === option.value} onChange={() => handleChange(option.value)} className={styles.input} id={`option-${option.value}`} // ✅ role="radio"とaria-checkedを追加 role="radio" aria-checked={selectedValue === option.value} /> {/* ✅ <label>要素でinputとラベルを適切に関連付け */} <label htmlFor={`option-${option.value}`} className={styles.label}> {option.label} </label> </li> ))} </ul> </div> ); };
提案されたコードの品質について
提案されたコードに関しては人の目でチェックする必要がありますが、改善されたポイントを見ると、APGガイドラインに準拠しようとしている意図は読み取れます。
そのまま使えるかに関しては課題があるものの、構造的な改善やARIA属性の追加など、方向性としては正しいステップを踏んでいることが確認できました。
レビュー結果と提案を受けて気になったこと
レビューを通じて、AIエージェントが提案してくれたコードは、確かにAPGに準拠した実装になっていました。
ただ、それを踏まえてひとつ気になったのは、「本当にそこまでカスタム実装が必要だったのか?」という点です。
提案されたコードでは、APGのExampleに沿って divベースでaria属性を多用したカスタム実装が採用されていました。
ですが、今回のようなシンプルなラジオボタングループであれば、ネイティブのinputを使うだけで、必要なアクセシビリティ要件は十分に満たせるのでは?という気づきがありました。
APGとの違いと使い分けの視点
APGのRadioGroupパターンでは、矢印キーでの移動やフォーカス制御、選択状態の管理などをより柔軟にカスタマイズしたいケースに対応するため、aria-activedescendantやroving tabindex を用いたカスタム実装が紹介されています。
これは、たとえば以下のような場面で有効です。
- div や li など、非ネイティブ要素で独自のUIを構築したい場合
- より自由なレイアウトやスタイリングが必要な場合
一方で、要件がネイティブ要素で自然に満たせる場合は、無理にAPGに準拠したカスタム実装に寄せすぎない方が、保守性や実装コストの面でも合理的だと感じました。
また、どこまでのアクセシビリティ対応が必要かについては、チームやプロジェクト内で方針を明確にし、共通認識を持っておくことも重要です。
CursorのRules機能を活用すれば、こうした方針をある程度AI側にも伝えることができそうなので、今後はチームの判断基準をAIにも反映させていく可能性についても探っていきたいと思います。
感想
今回の検証を通じて、AIエージェントは専門的な知識を活用して具体的な問題点を指摘し、課題の洗い出しや改善提案を一定レベルでサポートしてくれる、頼もしいサポート役になり得ると感じました。
実装への適用は取捨選択が必要ですが、課題の洗い出しをAIエージェントに任せることで属人性を下げ、作業効率も高められると感じています。
一方で、計算が求められる領域では精度に限界があるため、外部ツールとの併用や人のレビューが依然として重要です。
また、APGのようなガイドラインも、目的に応じて柔軟に取り入れることが大切であり、「すべてAI任せ」ではなく「活かすポイントを見極める目」が必要だと再確認しました。
今後の展望
今回は試すことができませんでしたが、axe-core/playwrightとCursorを活用したアクセシビリティ改善の可能性についても今後探っていきたいと思っています。
アクセシビリティに関するテストとAIエージェントを組み合わせることで、より効率的で包括的なアクセシビリティ改善が実現できるのではないかと期待しています。

