Web開発において、セマンティックコーディングとアクセシビリティは重要な要素です。この記事では、role
属性とaria
属性を中心に、実際のコード例とベストプラクティスを交えて解説していきます!
TL;DR → セマンティックHTMLを基本とし、必要に応じてrole属性とaria属性を組み合わせることで、アクセシブルで保守性の高いコードが書ける
目次
セマンティックコーディングとは
定義
セマンティックコーディングとは、HTMLの要素に意味を持たせ、コンテンツの構造と目的を明確にすることです。
メリット
エンジニアや閲覧者だけでなく、機械にとっても構造がわかりやすいページを作るということなので、以下の良いことがあります!
- スクリーンリーダー、リッチリザルト、音声検索、AI要約などで扱いやすくなる
- SEOの向上
- コードの可読性向上
- メンテナンス性の向上
- etc
悪い例 vs 良い例
<!-- ❌ 悪い例 -->
<div class="header">
<div class="nav">
<div class="main">
<div class="footer">
<!-- ✅ 良い例 -->
<header>
<nav>
<main>
<footer>
セマンティックHTMLを使うことで、ブラウザやスクリーンリーダーが要素の役割を理解しやすくなります!
role属性の基礎と実践
role属性とは
role
属性は、HTML要素に明示的な役割を定義する属性です。スクリーンリーダーが要素の役割を理解するのに役立ちます。
ただし、重要な注意点があります:
-
role
属性は要素の意味的な役割を定義するだけで、機能は追加しません -
role="button"
を付けても、その要素が自動的にフォーカス可能になったり、キーボード操作が可能になるわけではありません - 完全なアクセシビリティを実現するには、JavaScriptでの実装も必要です
よく使うrole属性
role属性 | 説明 | 使用例 |
---|---|---|
button |
ボタンとして扱う | <div role="button"> |
navigation |
ナビゲーション | <div role="navigation"> |
main |
メインコンテンツ | <div role="main"> |
complementary |
補足情報 | <div role="complementary"> |
banner |
ヘッダー | <div role="banner"> |
role属性の実際の動作を確認してみよう
以下の2つのボタンを比較してみましょう:
<!-- 通常のbutton要素 -->
<button>通常のボタン</button>
<!-- div要素にrole="button"を追加 -->
<div role="button">role属性付きボタン</div>
<!-- div要素(role属性なし) -->
<div>role属性なしのdiv</div>
通常のbutton要素:button role、focusableが自動的に設定される
role="button"付きdiv要素:roleは設定されるが、focusableはfalseのまま
role属性なしdiv要素:role="generic"(デフォルト)、focusable=""
重要な発見
-
<button>
要素:role="button"
とfocusable="true"
が自動的に設定される -
<div role="button">
:role="button"
は設定されるが、focusable="false"
のまま -
<div>
要素(role属性なし):role
もfocusable
も設定されない(genericはdivのデフォルトのrole) - role属性だけでは不十分: 完全なアクセシビリティには追加の実装が必要(この場合は、tabindex="0"を付与してフォーカス可能にするべき)
セマンティックHTML vs role属性
セマンティックHTMLの例
<header>
<h1>ヘッダー</h1>
<nav>
<a href="#">ホーム</a>
<a href="#">サービス</a>
<a href="#">お問い合わせ</a>
</nav>
</header>
<main>
<h2>メインコンテンツ</h2>
<p>ここにメインの内容が入ります。</p>
</main>
<aside>
<h3>サイドバー</h3>
<p>補足情報</p>
</aside>
<footer>
<p>© 2024 セマンティックコーディング</p>
</footer>
role属性を使用した例
<div role="banner">
<h1>ヘッダー (role="banner")</h1>
<div role="navigation">
<a href="#">ホーム</a>
<a href="#">サービス</a>
<a href="#">お問い合わせ</a>
</div>
</div>
<div role="main">
<h2>メインコンテンツ (role="main")</h2>
<p>ここにメインの内容が入ります。</p>
</div>
<div role="complementary">
<h3>サイドバー (role="complementary")</h3>
<p>補足情報</p>
</div>
<div role="contentinfo">
<p>© 2024 セマンティックコーディング</p>
</div>
✅ セマンティックHTML
メリット
- ブラウザ・スクリーンリーダーに標準で意味が伝わる
- SEOに有利(検索エンジンが意味を理解しやすい)
- コードの可読性・保守性が高い
- パフォーマンスが良い(追加属性不要)
- アクセシビリティ機能が自動で提供される(例:
<button>
なら自動的に Enter/Space キー対応)
デメリット
- 意味のあるタグが用意されていないケースには対応できない
- カスタムUIを作り込みたいときに標準挙動を上書きするのが大変
✅ role属性
メリット
- セマンティックHTMLにない複雑UI(タブ、アコーディオン、モーダル等)も定義できる
- 古いコードや非セマンティック要素を「後付け」で意味づけできる
- 後方互換性や柔軟なカスタマイズに強い
デメリット
- 機能は手動で実装する必要がある(キーボード操作や状態管理は自分で作らないとダメ)
- 実装が複雑になりがち
- 間違って付けると逆にアクセシビリティを壊す(誤用リスク)
- メンテナンスが大変
aria属性の基礎と実践
aria属性とは
aria
属性は、Accessible Rich Internet Applicationsの略で、Webアプリケーションのアクセシビリティを向上させる属性群です。
これを使用することで、標準であるタグやrole属性だけでは表すことができないものを表すことができます!
よく使うaria属性
aria属性 | 説明 | 使用例 |
---|---|---|
aria-label |
要素の名前(ラベル)を上書きする | aria-label="メニューを開く" |
aria-describedby |
補足説明の参照(IDを指定) | aria-describedby="help-text" |
aria-hidden |
アクセシビリティツリーから除外する | aria-hidden="true" |
aria-expanded |
展開状態(トグル制御側に付与) | aria-expanded="false" |
aria-pressed |
押下状態(トグルボタン用) | aria-pressed="false" |
aria-controls |
制御対象の要素IDを関連付ける | aria-controls="panel-1" |
実践的な例
アコーディオン (Accordion)
✅ セマンティック版(<details>
/<summary>
)
<section aria-label="よくある質問">
<details>
<summary>質問1:配送はどのくらいかかりますか?</summary>
<p>通常2〜3営業日でお届けします。</p>
</details>
<details>
<summary>質問2:返品は可能ですか?</summary>
<p>到着から7日以内であれば返品可能です。</p>
</details>
</section>
✅ カスタムUI版(<button>
+ARIA)
<section class="accordion" aria-label="よくある質問">
<h3>
<button class="acc-trigger" aria-expanded="false" aria-controls="acc-p1" id="acc-h1" type="button">
質問1:配送はどのくらいかかりますか?
</button>
</h3>
<div id="acc-p1" class="acc-panel" role="region" aria-labelledby="acc-h1" hidden>
<p>通常2〜3営業日でお届けします。</p>
</div>
<h3>
<button class="acc-trigger" aria-expanded="false" aria-controls="acc-p2" id="acc-h2" type="button">
質問2:返品は可能ですか?
</button>
</h3>
<div id="acc-p2" class="acc-panel" role="region" aria-labelledby="acc-h2" hidden>
<p>到着から7日以内であれば返品可能です。</p>
</div>
</section>
<script>
const triggers = document.querySelectorAll('.acc-trigger');
triggers.forEach(btn => {
btn.addEventListener('click', () => {
const expanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!expanded));
document.getElementById(btn.getAttribute('aria-controls'))
.toggleAttribute('hidden', expanded);
});
btn.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); btn.click(); }
});
});
</script>
タブ (Tabs)
※ タブ用のセマンティック要素はないため、ARIAパターンのみ書いてます。
✅ カスタムUI版(Tabs ARIA)
<div class="tabs" role="tablist" aria-label="商品情報">
<button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1" tabindex="0">概要</button>
<button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">仕様</button>
<button role="tab" id="tab-3" aria-selected="false" aria-controls="panel-3" tabindex="-1">レビュー</button>
</div>
<section id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">概要の内容…</section>
<section id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden>仕様の内容…</section>
<section id="panel-3" role="tabpanel" tabindex="0" aria-labelledby="tab-3" hidden>レビューの内容…</section>
<script>
const tabs = document.querySelectorAll('[role="tab"]');
const panels = document.querySelectorAll('[role="tabpanel"]');
function activate(tab) {
tabs.forEach(t => { t.setAttribute('aria-selected', 'false'); t.tabIndex = -1; });
panels.forEach(p => p.hidden = true);
tab.setAttribute('aria-selected', 'true');
tab.tabIndex = 0;
const panel = document.getElementById(tab.getAttribute('aria-controls'));
panel.hidden = false;
tab.focus();
}
tabs.forEach(t => {
t.addEventListener('click', () => activate(t));
t.addEventListener('keydown', e => {
const i = Array.from(tabs).indexOf(t);
if (e.key === 'ArrowRight') activate(tabs[(i+1)%tabs.length]);
if (e.key === 'ArrowLeft') activate(tabs[(i-1+tabs.length)%tabs.length]);
if (e.key === 'Home') activate(tabs[0]);
if (e.key === 'End') activate(tabs[tabs.length-1]);
});
});
</script>
ポイント: role="tablist"
/ role="tab"
/ role="tabpanel"
、aria-selected
と aria-controls
、非表示は hidden
、キーボード(左右・Home/End)。
モーダル (Modal / Dialog)
✅ セマンティック版(<dialog>
)
<button id="openDlg">モーダルを開く</button>
<dialog id="dlg" aria-labelledby="dlg-title" aria-describedby="dlg-desc">
<h2 id="dlg-title">お知らせ</h2>
<p id="dlg-desc">新しい機能が追加されました。</p>
<form method="dialog">
<button>閉じる</button>
</form>
</dialog>
<script>
const dlg = document.getElementById('dlg');
const trigger = document.getElementById('openDlg');
trigger.addEventListener('click', () => dlg.showModal());
dlg.addEventListener('close', () => trigger.focus());
</script>
注意: <dialog>
要素は比較的新しく、サポート状況に差があります(IEは非対応、Safariも最近まで不完全)。そのため、本番環境ではカスタムUI版の使用を検討してください。
✅ カスタムUI版(role="dialog"
+aria-modal
)
<button id="openModal" aria-haspopup="dialog" aria-controls="modal">モーダルを開く</button>
<div id="modal" role="dialog" aria-modal="true"
aria-labelledby="modal-title" aria-describedby="modal-desc" hidden>
<div class="modal-surface">
<h2 id="modal-title">お知らせ</h2>
<p id="modal-desc">新しい機能が追加されました。</p>
<button id="closeModal" aria-label="モーダルを閉じる">閉じる</button>
</div>
</div>
<script>
const modal = document.getElementById('modal');
const openBtn = document.getElementById('openModal');
const closeBtn = document.getElementById('closeModal');
const focusable = () => modal.querySelectorAll('a,button,input,select,textarea,[tabindex]:not([tabindex="-1"])');
function openModal() {
modal.hidden = false;
const first = focusable()[0];
(first || closeBtn).focus();
document.addEventListener('keydown', trap);
}
function closeModal() {
modal.hidden = true;
document.removeEventListener('keydown', trap);
openBtn.focus();
}
function trap(e) {
if (e.key === 'Escape') { e.preventDefault(); closeModal(); return; }
if (e.key !== 'Tab') return;
const list = Array.from(focusable());
if (!list.length) return;
const i = list.indexOf(document.activeElement);
const next = e.shiftKey ? (i <= 0 ? list[list.length-1] : list[i-1]) : (i === list.length-1 ? list[0] : list[i+1]);
e.preventDefault(); next.focus();
}
openBtn.addEventListener('click', openModal);
closeBtn.addEventListener('click', closeModal);
</script>
ポイント: role="dialog"
aria-modal="true"
、開閉時のフォーカス移動、Tab のフォーカストラップ、Esc で閉じる。
フォームの例
aria-describedby
を使用したアクセシブルなフォームです。
<form class="demo-form">
<div class="form-group">
<label for="name">名前</label>
<input type="text" id="name" name="name"
aria-describedby="name-help" required>
<div id="name-help" class="help-text">
フルネームを入力してください
</div>
</div>
<div class="form-group">
<label for="email">メールアドレス</label>
<input type="email" id="email" name="email"
aria-describedby="email-help" required>
<div id="email-help" class="help-text">
有効なメールアドレスを入力してください
</div>
</div>
</form>
補足: この例では <label>
とセットで使用しているため、aria-describedby
がなくても最低限のアクセシビリティは確保されています。aria-describedby
は補助的な情報(ヒントやエラーメッセージ表示など)を提供するために使用します。
まとめ
✅ やるべきこと
-
意味のあるHTML要素を優先する
-
<button>
、<nav>
、<main>
などのセマンティック要素を積極的に使用 - セマンティック要素はアクセシビリティ機能が自動で提供される
-
-
role属性は必要最小限に
- セマンティックHTMLで表現できない場合のみ使用
- role属性だけでは不十分、JavaScriptでの実装も必要
-
aria属性は適切に組み合わせる
- 単体ではなく、関連する属性と組み合わせて使用
-
実際にテストする
- スクリーンリーダーやキーボードナビゲーションで確認
- 開発者ツールのAccessibilityタブでroleやfocusableを確認
❌ 避けるべきこと
-
過度なrole属性の使用
- セマンティックHTMLで表現できる場合は不要
-
aria属性の誤用
- 仕様に沿わない使用方法は避ける
-
テストなしでの実装
- アクセシビリティは実装後に必ずテスト
-
アクセシビリティの後回し
- 開発の最初から考慮する
テスト方法
-
スクリーンリーダーでの確認
- NVDA、JAWS、VoiceOverなどのスクリーンリーダーでテスト
-
キーボードナビゲーション
- マウスを使わずにキーボードだけで操作できるか確認
-
ブラウザの開発者ツール
- 要素のrole属性やaria属性が正しく設定されているか確認
-
アクセシビリティチェッカー
- axe、WAVEなどのツールで自動チェック