はじめに
「ウェブアクセシビリティって聞いたことはあるけど、実際に何をすればいいのか分からない」そんな方のために、React.js を使った具体的なコード例つきでアクセシビリティ対応のポイントをまとめました。
NGパターンとOKパターンを比較しながら、「どう実装すればよいか」がパッと見てわかるようにしています。実務ですぐに使えるような形式にしているので、アクセシビリティ対応の第一歩として、ぜひ活用してください。
対象者
- アクセシビリティ対応に興味があるが、まだ手をつけられていないフロントエンドエンジニア
- React.js でUIを実装していて、配慮すべき点を知りたい方
- アクセシビリティの実装方法を実例で理解したい人
今回参考にしたサイト
※音声やアニメーションなどReact.jsのコードだけで表現しにくい項目に関しては除外してます
達成しないと利用者に重大な悪影響を及ぼすもの(重大)
どのようなサービスやコンテンツでも必ず達成すべき基準。実現できていないと、利用者がページから離脱したり、理解困難・誤操作・危険性が生じる可能性あり。
🔇 自動再生させないようにする(WCAG 2.0:1.4.2)
音声の自動再生や強制再生は避ける。音声が自動で流れる場合は 3秒以内 に収める、または 停止・ミュート機能をつける必要がある。
import { useRef } from "react";
function AccessibleVideo() {
const videoRef = useRef(null);
const toggleMute = () => {
const video = videoRef.current;
if (video) video.muted = !video.muted;
};
const togglePlay = () => {
const video = videoRef.current;
if (video) {
video.paused ? video.play() : video.pause();
}
};
return (
<div>
<video
ref={videoRef}
autoPlay
muted
aria-label="風景紹介ビデオ"
width={600}
>
<source src="/video/scenery.mp4" type="video/mp4" />
お使いのブラウザは video タグをサポートしていません。
</video>
<div>
<button onClick={toggleMute}>🔇 ミュート</button>
<button onClick={togglePlay}>⏸️ 再生/停止</button>
</div>
</div>
);
}
function InaccessibleVideo() {
return (
<video autoPlay>
<source src="/video/scenery.mp4" type="video/mp4" />
お使いのブラウザは video タグをサポートしていません。
</video>
);
}
🚪 袋小路に陥らせないようにする(WCAG 2.0:2.1.2)
キーボードのみで操作しているユーザーが、フォーカスから抜け出せなくなるUI(例:モーダルダイアログ)を作らない。
閉じるボタンなどで キーボード操作だけでも脱出可能にすることが重要。
import { useRef, useEffect } from "react";
function HelpModal({ isOpen, onClose }) {
const closeBtnRef = useRef(null);
useEffect(() => {
if (isOpen) {
closeBtnRef.current?.focus(); // 初回フォーカスをボタンへ
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="helpTitle"
className="modal"
>
<h2 id="helpTitle">ヘルプ</h2>
<p>操作方法をご案内します。</p>
<button ref={closeBtnRef} onClick={onClose}>
閉じる
</button>
</div>
);
}
function BadModal() {
return (
<div role="dialog" aria-modal="true">
<h2>ヘルプ</h2>
<p>操作方法をご案内します。</p>
{/* ❌ フォーカス制御なし、閉じる手段なし */}
</div>
);
}
🔁 自動でコンテンツを切り替えるようにする(WCAG 2.0:2.2.2)
スライドショーやカルーセルなど、自動で変わるコンテンツには以下の機能が必要。
- 一時停止
- 停止
- 非表示
動き続けるコンテンツは他の操作や閲覧を妨げるため、制御手段を用意するのが必須。
import { useEffect, useRef, useState } from "react";
function AccessibleCarousel() {
const slides = ["スライド1", "スライド2", "スライド3"];
const [current, setCurrent] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCurrent((prev) => (prev + 1) % slides.length);
}, 5000);
return () => clearInterval(intervalRef.current);
}, []);
const stopCarousel = () => clearInterval(intervalRef.current);
return (
<div>
<div aria-live="polite" role="region">
<p>{slides[current]}</p>
</div>
<button onClick={stopCarousel}>⏸️ 一時停止</button>
</div>
);
}
import { useEffect, useRef, useState } from "react";
function InaccessibleCarousel() {
const slides = ["スライド1", "スライド2", "スライド3"];
const [current, setCurrent] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrent((prev) => (prev + 1) % slides.length);
}, 4000);
return () => clearInterval(interval);
}, []);
return (
<div>
<div>
<p>{slides[current]}</p>
</div>
{/* ❌ 一時停止ボタンなし */}
</div>
);
}
必ず達成しなければならないもの(必須)
「非干渉」ほどではないが、満たさないと操作不能や情報伝達の欠落が起きる重要な基準。確実に対応する必要があります。
🖼️ 画像に代替テキストを付ける(WCAG 2.0:1.1.1)
- ロゴ、写真、図表など、画像が示している情報には代替テキスト(alt)をつける
- 画像の内容を簡潔に伝えるため、文脈に合わせて記述を調整する
- デコレーション目的の画像は alt="" にする
function ImageExamples() {
return (
<div>
{/* 意味のある画像 */}
<img src="/icons/mail.png" alt="メールを送信" />
{/* グラフ画像:周囲に補足あり */}
<img src="/graphs/sales.png" alt="2024年売上の推移(詳細は下記テーブル参照)" />
{/* 装飾目的の画像 */}
<img src="/decor/border.svg" alt="" aria-hidden="true" />
</div>
);
}
⌨️ キーボード操作だけですべての機能にアクセスできるようにする(WCAG 2.0:2.1.1 / 2.4.3 / 3.2.1 など)
Tabキー操作で、すべてのインターフェース要素にアクセスさせる。
フォーカス時の表示、入力確定・キャンセル動作なども意図通りに制御する。
import { useState } from "react";
function SearchInput() {
const [query, setQuery] = useState("");
const suggestions = ["Tシャツ", "シャツ 綿100%", "長袖Tシャツ"];
return (
<div>
<label htmlFor="search">商品検索</label>
<input
id="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
aria-autocomplete="list"
aria-controls="suggestion-list"
/>
{query && (
<ul id="suggestion-list">
{suggestions.map((item, i) => (
<li key={i}>
<button onClick={() => setQuery(item)}>{item}</button>
</li>
))}
</ul>
)}
</div>
);
}
import { useState } from "react";
function InaccessibleSearchInput() {
const [query, setQuery] = useState("");
const suggestions = ["Tシャツ", "シャツ 綿100%", "長袖Tシャツ"];
return (
<div>
<label htmlFor="search">商品検索</label>
<input
id="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{query && (
<ul>
{suggestions.map((item, i) => (
<li key={i}>{item}</li> // ❌ クリックもできないただのテキスト
))}
</ul>
)}
</div>
);
}
🎨 色・文字の強調だけで情報を伝えない(WCAG 2.0:1.3.1 / 1.3.3 / 1.4.1)
赤字・太字・下線・拡大表示などの視覚的な強調だけでは、情報が伝わらない人もいる。
必須項目や注意点は、文章やマーク等で明示することが必要。
.sr-only は「視覚的に非表示だがスクリーンリーダーには読まれる」クラスです。Tailwind CSSなら sr-only クラスが用意されています。
function RequiredField() {
return (
<div>
<label htmlFor="address">
住所 <span style={{ color: "red" }} aria-hidden="true">※</span>
<span className="sr-only">(必須)</span>
</label>
<input id="address" required aria-required="true" />
</div>
);
}
function RequiredFieldBad() {
return (
<div>
<label htmlFor="address">
住所 <span style={{ color: "red" }}>※</span>
</label>
<input id="address" />
</div>
);
}
📖 スクリーンリーダーで意味が通じる順序にする(WCAG 2.0:1.3.1 / 1.3.2 / 2.4.3)
スクリーンリーダーはDOMツリー順(ソース順)で読み上げるため、視覚の配置に頼らず、論理的な順番で記述することが重要です。
function EventForm() {
return (
<form>
<div>
<label htmlFor="date">日時</label>
<select id="date" name="date">
<option value="today">にちようび</option>
</select>
</div>
<div>
<label htmlFor="place">場所</label>
<select id="place" name="place">
<option value="park">ぱしょ</option>
</select>
</div>
</form>
);
}
function BadLayout() {
return (
<form>
<div style={{ display: "flex" }}>
<p>日時:</p>
<span>にちようび</span>
<p>場所:</p>
<span>ぱしょ</span>
</div>
</form>
);
}
function ConsentForm() {
return (
<form>
<fieldset>
<legend>同意事項</legend>
<p>ご利用規約に同意のうえ、申し込んでください。</p>
<label>
<input type="checkbox" required /> 利用規約に同意する
</label>
</fieldset>
<button type="submit">同意して申し込む</button>
</form>
);
}
function BadConsentForm() {
return (
<form>
{/* ❌ 文脈より先にボタンがある */}
<button type="submit">同意して申し込む</button>
<fieldset>
<legend>同意事項</legend>
<p>ご利用規約に同意のうえ、申し込んでください。</p>
<label>
<input type="checkbox" required /> 利用規約に同意する
</label>
</fieldset>
</form>
);
}
🧩 見出し要素だけで、セクションやブロックに含まれる要素を表現する(WCAG 2.0:1.3.1 / 2.4.6 / 2.4.10)
見出しは構造を正しく表現するために重要で、文書の意味をスクリーンリーダー等でも正しく伝えるためには適切なレベル(h1〜h6)を使うことが必要。
- 「リスト」など汎用的すぎる見出しは意味を成さない
- 内容に応じて階層的なレベル設定(h2→h3など)を行う
- 強調のためだけに見出しタグを使わない
function RecipeSection() {
return (
<section>
<h2>お菓子のレシピリスト</h2>
<ul>
<li>クッキー</li>
<li>ミートパイ</li>
<li>バナナマフィン</li>
<li>チーズケーキ</li>
</ul>
</section>
);
}
function InaccessibleList() {
return (
<section>
<h2>リスト</h2>
<ul>
<li>クッキー</li>
<li>ミートパイ</li>
<li>バナナマフィン</li>
<li>チーズケーキ</li>
</ul>
</section>
);
}
🎯 文字と背景の間に十分なコントラスト比を保つ(WCAG 2.0:1.4.3)
視認性を保つため、文字と背景色のコントラスト比を十分に確保する。
- 小さな文字では 4.5:1以上
- 大きな文字(18pt以上 or 14pt太字)では 3:1以上
function HighContrastText() {
return (
<div style={{ backgroundColor: "#333", color: "#fff", padding: "1rem" }}>
コントラストが適切なテキストです
</div>
);
}

function LowContrastText() {
return (
<div style={{ backgroundColor: "#ffffff", color: "#aaaaaa", padding: "1rem" }}>
コントラストが低すぎるテキストです
</div>
);
}

🔍 テキストの拡大縮小をしても情報が読めるようにする(WCAG 2.0:1.4.4)
ブラウザの文字拡大機能で200%まで拡大してもレイアウトが崩れず、内容が読めるように設計する。
- user-scalable=no は避ける(拡大を妨げる)
- レイアウトは柔軟に可変対応(rem / em 単位推奨)
function ResponsiveText() {
return (
<div style={{ fontSize: "1rem", lineHeight: 1.5 }}>
このテキストはユーザーがブラウザ拡大しても崩れません。
</div>
);
}
function FixedText() {
return (
<>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<div style={{ fontSize: "12px" }}>
❌ スマートフォンで拡大できず、読みづらい
</div>
</>
);
}
🧾 ページの内容を示すタイトルを適切に表現する(WCAG 2.0:2.4.2)
- タイトルはそのページの内容を表すように設定する
- 全ページで同じタイトルを使い回すのはNG
- ページタイトルとページ見出し(h1)が一致していると、スクリーンリーダー利用者にも安心感を与える
import { Helmet } from "react-helmet";
function RecipePage() {
return (
<>
<Helmet>
<title>トマトパスタのレシピ</title>
</Helmet>
<main>
<h1>トマトパスタのレシピ</h1>
<p>美味しいトマトソースの作り方を紹介します。</p>
</main>
</>
);
}
import { Helmet } from "react-helmet";
function RecipePage() {
return (
<>
<Helmet>
<title>レシピ情報</title> {/* ❌ 複数ページで同一タイトル */}
</Helmet>
<main>
<h1>トマトパスタのレシピ</h1>
</main>
</>
);
}
🔗 リンクを適切に表現する(WCAG 2.0:2.4.4)
- 「詳しくはこちら」「もっと見る」などの曖昧なリンクテキストは避ける
- スクリーンリーダーではリンクテキストのみが一覧表示されることが多いため、リンク先の内容が伝わる表現にする
function TrashGuideLinks() {
return (
<div>
<ul>
<li><a href="/garbage/bulky">粗大ごみの出し方の詳細</a></li>
<li><a href="/garbage/recyclable">資源ごみの出し方の詳細</a></li>
</ul>
</div>
);
}
function BadLinks() {
return (
<div>
<ul>
<li><a href="/garbage/bulky">詳しくはこちら</a></li>
<li><a href="/garbage/recyclable">詳しくはこちら</a></li>
</ul>
</div>
);
}
🧭 ナビゲーションに一貫性をもたせる(WCAG 2.0:3.2.3)
- サイトのグローバルナビゲーションは、すべてのページで同じ順序・構造にする
- 項目の並びやラベル、配置位置が変わると、利用者が混乱する
function Navigation() {
return (
<nav aria-label="メインナビゲーション">
<ul>
<li><a href="/top">トップ</a></li>
<li><a href="/about">施設紹介</a></li>
<li><a href="/price">料金</a></li>
<li><a href="/contact">お問い合わせ</a></li>
</ul>
</nav>
);
}
function InconsistentNav() {
return (
<nav>
{/* ❌ トップページと別ページで項目・順番・表現が不一致 */}
<ul>
<li><a href="/help">ヘルプ</a></li>
<li><a href="/about">サービス紹介</a></li>
<li><a href="/home">ホーム</a></li>
</ul>
</nav>
);
}
🧩 同じ機能には、同じラベルや説明をつける(WCAG 2.0:3.2.4)
- 同じ意味・同じ動作をするUIには、常に同じラベル・アイコン・説明を使う
- 見た目や文言がページごとに違うとユーザーが混乱する
function ReceiptButton() {
return (
<button aria-label="領収書をダウンロード">領収書をダウンロード</button>
);
}
function InconsistentReceiptButton({ context }: { context: string }) {
return (
<>
{context === "履歴" ? (
<button aria-label="明細書を出力">明細書を出力</button> // ❌ 意味は同じだが表現が異なる
) : (
<button aria-label="パソコンに保存">パソコンに保存</button> // ❌ ラベルの一貫性なし
)}
</>
);
}
状況に応じて確認すべきこと(個別対応)
すべてのウェブサイトや情報システムに一律に求められるわけではないが、対象や利用状況によっては配慮が必要となるアクセシビリティ基準のことです。
📝 入力フォームを様々な使い方でも使えるようにする(WCAG 2.0:1.3.1 / 3.3.1 / 3.3.2)
- ラベルは必ず明示的に付ける
- 入力制限(例:全角・半角など)は明示
- エラー発生時は箇所と内容を具体的に表示
- スクリーンリーダーでも読まれるようにする
- 入力内容確認・取消などの配慮
function InquiryForm() {
return (
<form>
<label htmlFor="inquiryNumber">お問合せ番号(半角)</label>
<input id="inquiryNumber" type="text" pattern="\d*" />
<label htmlFor="inquiryContent">
お問合せ内容 <span aria-hidden="true">(必須)</span>
</label>
<textarea
id="inquiryContent"
required
maxLength={500}
aria-describedby="contentLimit"
/>
<p id="contentLimit">0/500</p>
</form>
);
}
<input type="text" placeholder="お問合せ番号を入力" />
🌀 コンテンツの変化がスクリーンリーダーにも分かるようにする(WCAG 2.0 達成基準:2.2.1、3.2.2、3.2.5)
- ページ内の 一部の更新や状態変化(例:ダイアログ、トースト、チェック状態など)も、スクリーンリーダーで正しく伝える必要がある
- フォーカスの適切な移動や、ARIA属性の活用が重要
- モーダルを開いたらフォーカスをモーダル内に移動し、閉じたら元に戻すのが基本
✅ OKのポイント
-
role="dialog"
やaria-modal="true"
でモーダルを支援技術に明示 -
focus()
によるフォーカス誘導 - 終了後にフォーカスを戻す
import { useEffect, useRef, useState } from "react";
function AccessibleModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const cancelButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) {
cancelButtonRef.current?.focus(); // モーダル内に初期フォーカス
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="accessible-dialog-title"
className="modal"
>
<h2 id="accessible-dialog-title">本当にメッセージを削除しますか?</h2>
<button ref={cancelButtonRef} onClick={onClose}>キャンセル</button>
<button onClick={() => { alert("削除しました"); onClose(); }}>削除</button>
</div>
);
}
export default function AccessibleModalExample() {
const deleteBtnRef = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState(false);
const handleClose = () => {
setIsOpen(false);
deleteBtnRef.current?.focus(); // 元の操作位置に戻す
};
return (
<div style={{ padding: "2rem" }}>
<h1>アクセシブルなモーダル</h1>
<button ref={deleteBtnRef} onClick={() => setIsOpen(true)}>
削除
</button>
<AccessibleModal isOpen={isOpen} onClose={handleClose} />
</div>
);
}
❌ NGの問題点
- モーダルがスクリーンリーダーに認識されない
- フォーカス制御がなく、キーボード操作での利用が困難
import { useState } from "react";
function InaccessibleModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
if (!isOpen) return null;
return (
<div className="modal">
<h2>削除しますか?</h2>
{/* ❌ アクセシビリティ属性なし、フォーカス制御なし */}
<button onClick={onClose}>キャンセル</button>
<button onClick={() => { alert("削除しました"); onClose(); }}>削除</button>
</div>
);
}
export default function InaccessibleModalExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<div style={{ padding: "2rem" }}>
<h1>アクセシビリティ非対応モーダル</h1>
<button onClick={() => setIsOpen(true)}>削除</button>
<InaccessibleModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
</div>
);
}
導入に慎重な検討が必要(非推奨)
アクセシビリティを向上させるつもりでも、実際には支援技術と衝突したり、逆効果になるケースがあります。以下のような技術の導入は慎重に検討してください。
文字サイズの変更、読み上げプラグインの利用は非推奨
ブラウザやOSの機能で対応可能な文字サイズ変更・読み上げなどを、サイト側で独自実装するのは避ける。支援技術ユーザーが自分の使い慣れた設定を使えなくなる可能性があります。
✅ OKな対応の考え方
- CSSは rem や em を使って拡大縮小に柔軟に対応
- 読み上げは aria 属性や HTML 構造による補完で対応
- ブラウザや支援技術が活かせる構造・設計にする
function FontSizeChanger() {
const changeFontSize = (size) => {
document.body.style.fontSize = size;
};
return (
<div>
<button onClick={() => changeFontSize("14px")}>文字サイズ 小</button>
<button onClick={() => changeFontSize("18px")}>文字サイズ 中</button>
<button onClick={() => changeFontSize("22px")}>文字サイズ 大</button>
</div>
);
}
おわりに
↑デジタル庁のウェブアクセシビリティのサイトが本当にわかりやすかったです。
ガイドブック(PDF)には画像付きで解説されているので是非見て見てください。
最後までお読みいただき、ありがとうございました!