Claude Codeでアクセシビリティ対応を実装する実践ワークフロー
Claude Codeでアクセシビリティ対応を進める手順。HTML、ARIA、フォーム、axe、実機確認まで解説。
アクセシビリティ対応は「最後にaxeを流す作業」ではありません。最初のHTML、キーボード操作、フォーカス、フォームのエラー、色、手動確認までを小さく積み上げる開発習慣です。
Claude Codeはこの習慣を支える強力な相棒になります。ただし、曖昧に「アクセシブルにして」と頼むだけでは、divにARIAを足しただけの危ない差分や、見た目は変わらないがキーボードで詰まるUIを作ることがあります。
この記事では、初心者でも使える順番で、Claude Codeに任せる範囲と人間が確認する範囲を分けます。基準はW3CのWCAG 2.2、実装パターンはWAI-ARIA Authoring Practices Guide、ARIAの考え方はMDNのARIAガイド、自動検査はDequeのaxe-coreドキュメントを参照します。Claude Code自体の基本は公式ドキュメントを確認してください。
まず到達点を決める
実務では「完璧なアクセシビリティ」ではなく、公開前に落としてはいけない問題を決めるところから始めます。目標をぼかすと、Claude Codeは広すぎる修正を提案しやすくなります。
この記事のおすすめ到達点は次です。
| 領域 | 最低限の合格ライン | よくある失敗 |
|---|---|---|
| セマンティックHTML | button, a, form, label, main, navを意味どおり使う | クリックできるdivを量産する |
| キーボード操作 | Tab、Shift+Tab、Enter、Space、Escapeで主要操作ができる | マウスだけ閉じられるモーダル |
| フォーカス管理 | 開いたUIにフォーカスが入り、閉じたら元のボタンに戻る | フォーカスがページ裏側へ逃げる |
| ARIA | ネイティブHTMLで足りない場合だけ使う | aria-labelで見出しやラベル不足をごまかす |
| 色と状態 | WCAG AA相当のコントラストと、色以外の手がかりがある | 赤文字だけでエラーを表す |
| フォーム | ラベル、説明、エラーが入力欄と結びつく | エラーが視覚的には出るが読み上げられない |
| 検査 | axeなどの自動検査と、キーボード・スクリーンリーダーの手動確認を両方行う | 自動検査ゼロ件だけで合格にする |
特に重要なのは「ARIAは補助であり、土台ではない」という考え方です。MDNも、必要な意味と挙動を持つネイティブHTMLがあるなら、ARIAで置き換えるよりそちらを使うべきだと説明しています。
Claude Codeに渡す安全なプロンプト
アクセシビリティ修正では、作業範囲、基準、禁止事項、検証方法をプロンプトに入れます。次の形なら、無関係なリファクタリングや見た目だけの修正を抑えやすくなります。
claude <<'PROMPT'
Scope:
- Review only src/components/CheckoutForm.tsx and its tests.
- Do not change pricing copy, analytics events, or unrelated styles.
Accessibility target:
- Use WCAG 2.2 AA as the practical target.
- Prefer semantic HTML before ARIA.
- Add ARIA only when native HTML cannot express the state.
Check these items:
- Labels, descriptions, required state, and validation errors.
- Keyboard operation with Tab, Shift+Tab, Enter, Space, and Escape.
- Focus order, visible focus, and focus return after closing UI.
- Color contrast and non-color error indicators.
- Automated axe check plus manual screen-reader notes.
Output:
- Findings first, with file and line references.
- Minimal patch.
- Commands to verify.
- Any remaining risk.
PROMPT
このプロンプトのポイントは、Claude Codeに「コード生成」だけでなく「レビュー担当」として働かせることです。先に問題一覧を出させると、なぜその差分が必要なのかをレビューしやすくなります。
ユースケース1: LPや記事CTAをセマンティックに直す
最初の実例は、商品ページや記事末尾のCTAです。ここは収益導線なので、見た目を壊さずにボタン、リンク、見出し構造を正しくする必要があります。
悪い例は次のような実装です。マウスでは動きますが、キーボード操作、リンク先の意味、スクリーンリーダーでの名前が弱くなります。
<div class="hero-card" onclick="location.href='/products'">
<div class="title">Claude Code Templates</div>
<div class="button">Buy now</div>
</div>
まずはHTMLの意味を戻します。カード全体を無理にクリック領域にせず、見出しと説明とリンクを明確にします。
<section aria-labelledby="templates-heading" class="product-cta">
<h2 id="templates-heading">Claude Codeテンプレートでレビューを短縮する</h2>
<p>
日々の実装、レビュー、デバッグで使うプロンプトを
コピペできる形にまとめています。
</p>
<a class="primary-link" href="/products">
教材一覧を見る
</a>
</section>
Claude Codeに依頼するときは、divをbutton風にするのではなく、リンクとして遷移するならa、画面内の状態を変えるならbuttonにするよう明記します。生産性Tipsで紹介したように、変更範囲を小さくするとCTAや計測タグを壊しにくくなります。
ユースケース2: フォームとエラーを読み上げ可能にする
問い合わせ、資料請求、購入前相談のフォームは、アクセシビリティと売上が直結します。入力欄にラベルがない、エラーが赤文字だけ、送信後にどこを直せばよいか分からない、という状態は離脱を生みます。
以下はReactでそのまま試せる最小フォームです。ラベル、説明、エラー、aria-invalid、aria-describedbyを入力欄に結びつけています。
import { FormEvent, useState } from "react";
type Errors = {
name?: string;
email?: string;
};
export function ConsultationForm() {
const [errors, setErrors] = useState<Errors>({});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const nextErrors: Errors = {};
if (!String(data.get("name") || "").trim()) {
nextErrors.name = "お名前を入力してください。";
}
if (!String(data.get("email") || "").includes("@")) {
nextErrors.email = "メールアドレスを確認してください。";
}
setErrors(nextErrors);
}
return (
<form aria-labelledby="consultation-title" onSubmit={handleSubmit} noValidate>
<h2 id="consultation-title">導入相談フォーム</h2>
<div className="field">
<label htmlFor="name">お名前</label>
<input
id="name"
name="name"
autoComplete="name"
aria-invalid={errors.name ? "true" : "false"}
aria-describedby={errors.name ? "name-error" : undefined}
/>
{errors.name && (
<p id="name-error" role="alert">
{errors.name}
</p>
)}
</div>
<div className="field">
<label htmlFor="email">メールアドレス</label>
<p id="email-help">返信できる業務用メールを入力してください。</p>
<input
id="email"
name="email"
type="email"
autoComplete="email"
aria-invalid={errors.email ? "true" : "false"}
aria-describedby={
errors.email ? "email-help email-error" : "email-help"
}
/>
{errors.email && (
<p id="email-error" role="alert">
{errors.email}
</p>
)}
</div>
<button type="submit">相談内容を送信する</button>
</form>
);
}
失敗しやすい点は、aria-describedby="email-error"を常に付けてしまうことです。エラー要素が存在しない状態で参照だけ残すと、保守時に混乱します。補足説明は常時、エラーは発生時だけ、という分け方にすると読みやすくなります。
ユースケース3: モーダル、コマンドパレット、メニュー
モーダルやコマンドパレットは、Claude Codeがもっともそれらしく作りやすく、同時に壊しやすいUIです。WAI-ARIA APGのDialog Modal Patternでは、開いたらフォーカスが内部に入り、Tabが内部を循環し、Escapeで閉じ、閉じた後は呼び出し元へ戻ることが示されています。
次の実装は小さなモーダルの土台です。複雑なアニメーションやポータルを入れる前に、この動作をテストします。
import { ReactNode, useEffect, useRef } from "react";
type ModalProps = {
open: boolean;
title: string;
onClose: () => void;
children: ReactNode;
};
const focusableSelector = [
"a[href]",
"button:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
'[tabindex]:not([tabindex="-1"])',
].join(",");
export function AccessibleModal(props: ModalProps) {
const { open, title, onClose, children } = props;
const dialogRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!open) return;
previousFocusRef.current = document.activeElement as HTMLElement;
const dialog = dialogRef.current;
const focusable = dialog?.querySelectorAll<HTMLElement>(focusableSelector);
focusable?.[0]?.focus();
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") onClose();
if (event.key !== "Tab" || !dialogRef.current) return;
const items = [...dialogRef.current.querySelectorAll<HTMLElement>(
focusableSelector
)];
const first = items[0];
const last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last?.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first?.focus();
}
}
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
previousFocusRef.current?.focus();
};
}, [open, onClose]);
if (!open) return null;
return (
<div className="modal-backdrop">
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="modal-panel"
>
<h2 id="modal-title" tabIndex={-1}>
{title}
</h2>
{children}
<button type="button" onClick={onClose}>
閉じる
</button>
</div>
</div>
);
}
メニューの場合はMenu Button Patternを参照します。ただし、単なるページ内ナビゲーションならnavとaのリストで足りることが多いです。ARIAメニューはアプリケーション風のコマンド一覧に向いており、通常のグローバルナビに使うと操作期待が変わってしまいます。
色、フォーカス、レスポンシブの最低ライン
色は自動検査で拾いやすい一方、実装で退化しやすい領域です。特にoutline: none、薄いグレー文字、ホバーだけの状態表示は避けます。
.primary-link {
background: #0f766e;
border-radius: 6px;
color: #ffffff;
display: inline-flex;
font-weight: 700;
gap: 0.5rem;
min-height: 44px;
padding: 0.75rem 1rem;
}
.primary-link:focus-visible,
button:focus-visible,
input:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
.field [role="alert"] {
border-left: 4px solid #b91c1c;
color: #7f1d1d;
margin-top: 0.5rem;
padding-left: 0.75rem;
}
44px前後のタップ領域を確保しておくと、モバイルでも操作しやすくなります。WCAG 2.2にはターゲットサイズに関する基準も追加されているため、PCだけで見て終わらせないことが重要です。
自動検査と手動スクリーンリーダー確認
axeは多くの構造的な問題を見つけますが、読み上げ順、説明の分かりやすさ、業務文脈に合ったラベルまでは保証しません。自動検査をCIに入れ、その後で人間がキーボードとスクリーンリーダーを確認します。
npm install -D @axe-core/playwright @playwright/test
npx playwright install --with-deps chromium
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
test("consultation form has no serious accessibility issues", async ({ page }) => {
await page.goto("/contact");
const results = await new AxeBuilder({ page })
.include("main")
.withTags(["wcag2a", "wcag2aa", "wcag22aa"])
.analyze();
expect(results.violations).toEqual([]);
});
name: accessibility-check
on:
pull_request:
jobs:
axe:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run build
- run: npx playwright test accessibility.spec.ts
手動確認では、WindowsならNVDA、macOSならVoiceOverから始めると現実的です。最初は全ページを完璧に読む必要はありません。CTA、フォーム、モーダル、ナビゲーション、エラー表示という収益と離脱に近い場所を優先します。
具体的な落とし穴
Claude Codeを使うときに実際に起きやすい失敗は次の通りです。
role="button"を足しただけで、Enter/Space操作を実装していない。- 画像に
alt="image"のような意味のない代替テキストを入れる。 - SVGアイコンだけのボタンに名前がない。
aria-hidden="true"を親に付け、モーダル自体まで隠してしまう。- エラー文を表示しているが、入力欄と
aria-describedbyでつながっていない。 - フォーカスリングをデザイン都合で消し、代替表示を用意していない。
- 自動検査の対象がログイン前ページだけで、実際の購入・相談フォームを見ていない。
このリストは、そのままClaude Codeへのレビュー依頼にも使えます。たとえば「上の失敗例がこの差分に含まれていないか、ファイルと行番号つきで確認して」と頼むと、単なる改善提案より実務的なレビューになります。
公開前チェックリスト
公開前は次の順番で見ます。
- 見出し階層が
h1から自然につながっている。 - クリックできる要素が
buttonまたはaになっている。 - Tabだけで主要導線を最後まで操作できる。
- フォーカス位置が常に見える。
- モーダルを開閉してフォーカスが戻る。
- フォームのラベル、説明、エラーが読み上げられる。
- 色だけに依存した状態表示がない。
- axeまたは同等ツールの重大な違反が残っていない。
- スクリーンリーダーでCTAとエラーの意味が分かる。
- 変更範囲外の文言、価格、計測イベントを触っていない。
Claude Codeのフックに検査を組み込みたい場合は、Claude Code hooks guideも参考になります。毎回の手作業を減らしつつ、最後の判断は人間が持つ形が安定します。
CTA: 学習から運用へ進める
アクセシビリティ対応を一度きりの修正で終わらせると、次のLP改修やフォーム追加でまた壊れます。ClaudeCodeLabでは、日常のコマンド習慣は無料チートシート、繰り返し使うレビュー文面はプロンプト集、チーム導入は相談という順番をおすすめしています。
実際に試した結果
この記事の更新では、Masaの実務でよく見る「フォームのエラーは見えているが読み上げられない」「モーダルを閉じた後にフォーカスがページ先頭へ飛ぶ」「CTAカードがdivクリックになっている」という3点をサンプルにして確認しました。Claude Codeには先に失敗例を列挙させ、その後に最小差分を作らせる流れが一番安定しました。axeの自動検査では拾えない文言の分かりやすさは、最後にキーボード操作とVoiceOver読み上げで人間が確認する必要があります。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Code権限セーフティラダー: 初心者がallowを広げる順番
Claude Codeの権限をread-onlyからbuild、限定編集、deploy確認まで段階的に広げる安全な運用手順。
Claude Code Small PR Proof Pack: 小さなPRをレビュー可能にする証拠セット
Claude Codeの小さなPRに、差分・検証・公開URL・CTA・rollbackを添える実務チェックリスト。
Claude Codeのコミット前レビューゲート: 差分、テスト、CTAをまとめて止める型
Claude Codeでcommit前に差分をレビューする実践手順。build、公開URL、CTA、Gumroadリンク、未翻訳本文を検知します。