アクセシビリティとは可能な限りの多くの人々が利用できるようにする状態のことを指します。
ウェブサイトで言えばスクリーンリーダーを利用している人や、モバイルデバイスで見る人、キーボード操作のみ行う方などすべての人が利用できるような状態です。
例えば、ハンバーガーメニューで「≡」のアイコンデザインをよく見かけませんか?あのアイコンにtabキーで移動でき、キーボード操作だけでメニューの開閉ができるサイトはアクセシビリティを意識しているのだなと思います。
首相官邸ホームページを見てみるとtabキーでメニューを移動できます。また画像の黄色い線で引いたところは、WAI-ARIAを使用していました。
アクセシビリティを高めて、多くの人に情報を届けようとしているなあと思いました。
アクセシビリティの向上には、HTMLの適切タグを使用したマークアップやWAI-ARIAの手法が挙げられます。
今回はWAI-ARIAについて紹介したいと思います。
WAI-ARIA(ウェイ アリア)とは
WAI-ARIAはWeb Accessibility Initiative Accessible Rich Internet Applicationsの略です。
W3Cによって定められた追加仕様で、HTMLだけでは表すことのできない構造や状態などを明示することができます。
WAI-ARIAにはコンテンツの役割を示すrole属性とコンテンツの状態・性質を示すaria属性が定義されています。
role属性
role属性に値を指定することで、要素が何か、または何をするかの「ロール」を設定できます。
buttonタグがrole=”button”のようにHTMLの各要素には、暗黙的にロールが規定されているものもあります。
ロールの注意点としては、
・暗黙なロールによっては、上書きできないロールがある
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <!-- 望ましくない --> <html> <body role="application"> <main> コンテンツ </main> </body> </html> <!-- 望ましい --> <html> <body> <div role="application"> <main> コンテンツ </main> </div> </body> </html> |
applicationロールは要素とその子要素をウェブアプリケーションと同様に扱うべき、と支援技術に指示します。
なので全体に適用されるようにbodyタグにロールを設定したくなるかもしれませんが、望ましくありません。
なぜならbodyタグに使っていいロールはないからです。mainタグも同様です。
bodyタグとmainタグの間にdivタグを挟みました。
divタグはすべてのロールを使用できるので、applicationロールを設定しました。
・要素に対応するロールがある場合は、ロールを使用するべきではない
divタグで書くように制限されている(CMS、JS、フレームワークなどによるとき)場合は仕方ないですが、そうでないのなら既に暗黙なロールを振られている要素に置き換えるべきです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <!-- 望ましくない --> <div role="button">ボタン</div> <div role="navigation"> <div role="list"> <div role="listitem"><a href="#">テキスト</a></div> <div role="listitem"><a href="#">テキスト</a></div> <div role="listitem"><a href="#">テキスト</a></div> <div role="listitem"><a href="#">テキスト</a></div> </div> </div> <!-- 望ましい --> <button>ボタン</button> <nav> <ul> <li><a href="#">テキスト</a></li> <li><a href="#">テキスト</a></li> <li><a href="#">テキスト</a></li> <li><a href="#">テキスト</a></li> </ul> </nav> |
・暗黙のロールを明示しない
暗黙のロールをrole属性を指定すると冗長になるので不要です。
1 2 3 4 5 6 7 8 9 10 11 | <!-- 望ましくない --> <nav role="navigation"> <ul role="list"> <li role="listitem"><a href="#">テキスト</a></li> <li role="listitem"><a href="#">テキスト</a></li> <li role="listitem"><a href="#">テキスト</a></li> <li role="listitem"><a href="#">テキスト</a></li> </ul> </nav> |
どのロールが使えるかどうかは、こちらを参照すると分かりやすいです。
日本語訳もあります。目を通しておくと勉強になります。
セマンティクスなコーディング、つまり文章の意味に合ったタグを使用するのが基本のコーディングかと思います。
WAI-ARIAは適切な要素を使えないとき・該当するロールを持つ要素が存在しないときに使う、など補助的な形で設定するのがよさそうです。
セマンティクスなコーディングについては、こちらの記事も参考にしてみてください。
SEOに強いホームページを作ろう!HTMLコーディング時に意識すること8つ
aria属性
aria属性はロールによって設定できるもの・できないものが決まっています。
なので暗黙なロールを定義されている要素にaria属性を設定する場合は、それが設定可能かどうか確認したほうがいいです。
aria属性ではプロパティとステートを設定することができます。プロパティは要素の性質を、ステートは要素の現在の状態を設定します。
ステートはJavaScriptによって操作し変更することで、ユーザーに要素の状態の変化を伝えることができます。
また、aria属性をCSSセレクタとして利用することもできます。
ヘッダーメニューが開いているときに「is-open」クラスをつけるなどして、状態によってクラスを付与することがあります。
そのクラスを付与する代わりにaria属性の値を変更し、aria属性の値によってスタイルを変更します。
1 2 3 4 5 6 7 8 9 | .content { display: none; } .content[aria-expanded="true"] { display: block; } |
具体例
いくつかのパーツを例に挙げながら、WAI-ARIAの属性の紹介をしていきます。
ボタン
1 2 3 | <button type="button"><i class="fas fa-shopping-cart"></i></button> |
アイコンだけのボタンを見かけることがあります。アイコンで内容に想像がつく場合が多いですが、スクリーンリーダーでサイトを見ている人には何のためのボタンか押すまで分かりません。
実際にスクリーンリーダーのNVDAで上記のボタンを読み上げてもらいました。すると「メインランドマーク ボタン」としか伝えられませんでした。
そこでaria-label属性で要素のラベルを設定してみました。
1 2 3 | <button type="button" aria-label="ショッピングカートに追加する"><i class="fas fa-shopping-cart"></i></button> |
すると「メインランドマーク ボタン ショッピングカートに追加する」をaria-label属性に設定した文言も読み上げるようになり、何のためのボタンかがスクリーンリーダーでも伝わります。
また似たような属性でaria-labelledby属性があります。
aria-label属性が値にラベルテキストを取るのに対して、aria-labelledby属性はラベルになる要素のidを指定します。
アコーディオン
上の動画のようにキーボード操作でアコーディオンが開閉できるようにしました。
コードは下記になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <ul class="accordion-list"> <li class="accordion"> <div class="accordion-header"> <button type="button" class="accordion-trigger" aria-expanded="true" aria-controls="accordion-panel-1">詳細を見る</button> </div> <div id="accordion-panel-1" class="accordion-panel" aria-hidden="false"> 内容 </div> </li> <li class="accordion"> <div class="accordion-header"> <button type="button" class="accordion-trigger" aria-expanded="false" aria-controls="accordion-panel-2">詳細を見る</button> </div> <div id="accordion-panel-2" class="accordion-panel" aria-hidden="true"> 内容 </div> </li> </ul> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | .accordion { list-style: none; } accordion + .accordion { margin-top: 20px; } .accordion-trigger { position: relative; } .accordion-trigger::before, .accordion-trigger::after { background-color: #333; content: ''; height: 2px; position: absolute; width: 15px; } .accordion-trigger::before { left: 15px; top: 48%; transform: rotate(0deg); } .accordion-trigger::after { left: 15px; top: 48%; transform: rotate(90deg); } .accordion-trigger[aria-expanded="true"]::before { transform: rotate(45deg); } .accordion-trigger[aria-expanded="true"]::after { transform: rotate(-45deg); } .accordion-panel { border: 1px solid #ccc; border-top: none; padding: 15px; } .accordion-panel[aria-hidden="true"] { display: none; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | var Accordion = function($el) { this.$trigger = $el.find('.accordion-trigger'); this.$panel = $el.find('.accordion-panel'); this.click(); }; // クリックイベント Accordion.prototype.click = function() { var _this = this; this.$trigger.on("click", function() { _this.toggle(); }); }; // アコーディオンの開閉処理の実行 Accordion.prototype.toggle = function() { var expandedFlg = (this.$trigger.attr('aria-expanded') === 'true'); if (expandedFlg) { this.close(); } else { this.open(); } }; // 閉じる Accordion.prototype.close = function() { this.$trigger.attr('aria-expanded', 'false'); this.$panel.attr('aria-hidden', 'true'); }; // 開く Accordion.prototype.open = function() { this.$trigger.attr('aria-expanded', 'true'); this.$panel.attr('aria-hidden', 'false'); }; if ($('.accordion').length > 0) { $('.accordion').each( function (index, elm) { new Accordion($(elm)); }); } |
buttonタグを使うのに違和感を覚えられた方もいるかと思います。
div.accordion-headerをクリックしてアコーディオンが開閉したのでも問題ない気がしますが、buttonタグの暗黙のロールbuttonは「ユーザーによってアクティブ化されたときに反応を引き起こすクリック可能な要素に使用する」とあるのでbuttonタグを使ってみました。
また、buttonタグとaタグはデフォルトでキーボード操作が有効になっているのも理由です。
buttonタグに以下2つのaria属性を指定しています。
・aria-expanded属性
要素、もしくは要素のグループが展開されているか、折りたためられているかの状態を示します。展開されている場合の値は「true」、折りたためられている場合は「false」
・aria-controls属性
要素が制御するidを値に指定し、制御する要素を明示します。今回だとアコーディオンの中身のidを指定しました。
アコーディオンの中身のdiv.accordion-panelには以下のaria属性を指定し、開いていないアコーディオンはスクリーンリーダーに読み上げられないようにしました。
・aria-hidden属性
要素が表示であるか非表示であるかの状態を示します。表示されている場合の値は「false」、非表示の場合は「true」
今回は、aria-expanded属性値と逆の値を取ります。
注意点ですが、JavaScriptでaria-expanded属性とaria-hidden属性の値を書き換えるのを忘れずに行ってください。
クリックしても自動的にaria属性の値は書き変わりません。もし値を書き換えるのを忘れると、アコーディオンを開いたはずなのに閉じているという間違った情報をユーザーに伝えることになります。
誤った情報を伝えることになるならaria属性を無理に指定しないほうがいいです。aria属性はうまく設定できればアクセシビリティの向上が望めますが、設定を間違えたらユーザーを混乱に陥れます。諸刃の剣ですね…。
タブ
上の動画のようにキーボード操作でタブが選択できるようにしました。
コードは下記になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <div class="tabs"> <ul role="tablist" class="tab-list"> <li role="presentation"> <button role="tab" id="tab-1" class="tab" aria-selected="true" aria-controls="panel-1" tabindex="0">タブ1</button> </li> <li role="presentation"> <button role="tab" id="tab-2" class="tab" aria-selected="false" aria-controls="panel-2" tabindex="-1">タブ2</button> </li> <li role="presentation"> <button role="tab" id="tab-3" class="tab" aria-selected="false" aria-controls="panel-3" tabindex="-1">タブ3</button> </li> </ul> <div role="tabpanel" id="panel-1" class="tab-panel" tabindex="0" aria-labelledby="tab-1" aria-hidden="false"> <p>パネル1</p> </div> <div role="tabpanel" id="panel-2" class="tab-panel" tabindex="0" aria-labelledby="tab-2" aria-hidden="true"> <p>パネル2</p> </div> <div role="tabpanel" id="panel-3" class="tab-panel" tabindex="0" aria-labelledby="tab-3" aria-hidden="true"> <p>パネル3</p> </div> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | .tab-list { display: flex; padding: 0; margin: 0; } .tab-list li { list-style: none; width: calc(100% / 3); } .tab { border: none; width: 100%; height: 50px; padding: 15px; } .tab[aria-selected="true"] { background-color: #ccc; } .tab-panel { border: 1px solid #ccc; border-top: none; padding: 50px; } .tab-panel[aria-hidden="true"] { display: none; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | var TabList = function() { var $el = $('.tabs'); this.$tablist = $el.find('.tab-list'); this.$tab = $el.find('.tab'); this.$panel = $el.find('.tab-panel'); this.click(); this.keydown(); }; // クリックイベント TabList.prototype.click = function() { var _this = this; this.$tab.on("click", function() { _this.isSelected($(this)); }); }; // キーボード操作 TabList.prototype.keydown = function() { var _this = this; var tabFocus = 0; this.$tablist.on("keydown", function(e) { // 右に移動 if (e.keyCode === 39 || e.keyCode === 37) { _this.$tab[tabFocus].setAttribute("tabindex", -1); if (e.keyCode === 39) { tabFocus++; // 最後にいる場合は、最初に移動します if (tabFocus >= _this.$tab.length) { tabFocus = 0; } // 左に移動 } else if (e.keyCode === 37) { tabFocus--; // 最初にいる場合は、最後に移動します if (tabFocus < 0) { tabFocus = _this.$tab.length - 1; } } _this.$tab[tabFocus].setAttribute("tabindex", 0); _this.$tab[tabFocus].focus(); } }); }; // タブの選択処理 TabList.prototype.isSelected = function($el) { var selectedFlg = ($el.attr('aria-selected') === 'true'); if (!selectedFlg) { this.reset(); this.select($el); } }; // リセット TabList.prototype.reset = function() { this.$tab.attr('aria-selected', 'false'); this.$panel.attr('aria-hidden', 'true'); }; // 選択処理 TabList.prototype.select = function($el) { $el.attr('aria-selected', 'true'); var panelId = $el.attr('aria-controls'); var $targetPanel = $('#' + panelId); $targetPanel.attr('aria-hidden', 'false'); }; if ($('.tabs').length > 0) { new TabList(); } |
タブに対応したロールが以下の3つほどあります。
・tablist
tabのリスト
・tab
タブ
・tabpanel
タブに関連するタブパネル
liタグには暗黙のロールでlistitemが定義されていますが、今回はulタグのロールがlistからtablistに変更しています。
そのためロール、listitemという情報は不要です。こんな風に暗黙のロールを打ち消したい場合は、role属性値をpresentationに設定します。
buttonタグに設定されている新しいaria属性は以下の2つです。
・aria-selected属性
要素が選択されているかどうかの状態を示します。
選択されている場合の値は「true」、選択されていない場合は「false」
・tabindex属性
要素がtabキーで選択できるかどうか制御します。
tabindex=”0″でタブ移動が可能となり、tabindex=”-1″でタブ移動ができなくなります。
buttonタグはtabキーで移動できるように自動でなっています。
今回はこちらを参考に、現在選択されているタブのみtabキーで移動でき、タブからタブへは矢印キーの左右で移動するようにしました。
tabindexの紹介のためにこのようなタブ移動にしましたが、tabキーでタブ移動してもいいような気がします。
個人的にはその方が使いやすいと思いました。
スクリーンリーダーへの対応
視覚的にコンテンツを見せたいが、スクリーンリーダーに読み上げられたくないときは、aria-hidden属性の値をtrueにすることで対応できます。
1 2 3 | <img src="/img/dummy.jpmg" alt="" aria-hidden="true"> |
hidden属性を設定したり、CSSで「display: none;」を指定したりすると要素自体が見えなくなるので、そのような場合はaria-hiddenで制御するといいみたいです。
またコンテンツとしては見えてほしくないが、スクリーンリーダーに読み上げてほしいときがあるかも知れません。
そのような場合は、以下のCSSで対応できます。
1 2 3 4 5 6 7 8 9 10 11 12 | .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } |
まとめ
WAI-ARIAについてどんなものなのか感じられたでしょうか?
気をつける点としては
・role属性によって指定できるaria属性を把握する
・aria属性のステートの値と要素の状態が一致する記述を書く
が挙げられるかなと思います。間違った指定をしてしまうとユーザーを混乱に陥れるので、上記の点を気を付けていただければと思います…。
下の参考記事や仕様等を確認して、どんなユーザーでも利用できるウェブサイトを構築していきましょう!
参考記事
WAI-ARIAの基本
WAI-ARIAを学ぶときに整理しておきたいこと
今からでも遅くない!誰も教えてくれなかった React とアクセシビリティーの世界
WAI-ARIA対応のタブ型UIの作り方 (基本編)
WAI-ARIAを活用したフロントエンド実装 第1回 role属性、aria属性の基礎知識
WAI-ARIAの正しい使い方 〜あるある?ケーススタディ〜
D2D アクセシビリティ勉強会 ~WAI-ARIAでアクセシブルにしてみよう~を開催しました。