アクセシビリティを意識したアコーディオンを作ってWAI-ARIAを学んでみたお話

公開:2020/12/10
更新:2020/12/10
14 min読了の目安(約12700字TECH技術記事 1

この記事は 「Webアクセシビリティ Advent Calendar 2020」 10日目の記事です。(執筆が終わったのが11日目になってしまい申し訳ございません)

先日投稿したWebアクセシビリティ Advent Calendarの記事「キーボード操作を意識したHTML/CSSコーディング」は思いがけず大変な反響をいただきました。ありがとうございます。

今回はWeb制作において頻出度の高いアコーディオンをアクセシビリティを意識しながら作ってみたという「やってみた」系記事です。

今回の記事を書くにあたり、そめさんよりいくつかレビューを頂きました(ありがとうございます)。今回はそのレビューも含めて修正前と修正後も同時掲載します。

はじめに

はじめに今回のアコーディオンのサンプルを掲載します。(サンプルではaria属性の付与はJSで行っています)

この記事を書いた理由は

  • WAI-ARIAの知識が少し不安だったので備忘録を兼ねて再学習記録を書こうと思った
  • こういった学習記録を書いておくと有識者からコメントをいただけるかもしれない

といったところです。

現代のHTMLではdetails要素とsummary要素を合わせて使えば簡単に開閉ができる折りたたみ式のウィジェットを作ることができますが、僕が受け持つ案件は大抵「IE対応」と「パネル開閉時のアニメーション」どちらも要求されがちで利用機会が無いので自作する必要があるんですよね…。

<details>
  <summary>見出し</summary>
  このテキストが折りたたまれます。こんな感じで。
</details>

今回のテーマはアコーディオンを作りながらWAI-ARIAを学ぶなのでdetails要素とsummary要素については今回はノータッチでいきます。今までは昔書いた汚いアコーディオンのテンプレートを使いまわしていましたが、これを機にアップデートしていこうという理由もあります。

WAI-ARIAとは?

まず、WAI-ARIAとはなんぞや?から。

WAI-ARIAは「Web Accessibility Initiative - Accessible Rich Internet Applications」の頭文字を取ったもので、HTMLやSVGに付与して利用できるアクセシビリティ確保のための属性の仕様です。

WAI

W3C内のWeb Accessibility Initiative(WAI)という団体・組織の名称で、標準のブラウザだけでなく様々なユーザーエージェントを用いてWebを利用する方々のためにウェブアクセシビリティの向上を目的としている団体です。

『Web Content Accessibility Guidelines』というウェブアクセシビリティに関する指針はWAIによって公開されています。

ARIA

WAIによって公開されているウェブアクセシビリティに関する仕様書です。
ウェブアクセシビリティへの考え方からアクセシビリティを満たすコーディング方法まで記載されています。

■参考文献:Accessible Rich Internet Applications (WAI-ARIA) 1.1 日本語訳

WAI-ARIAの2種類の属性

WAI-ARIAではrole属性aria属性という2種類の属性が定義されています。

role属性

簡単に言えばこれは見出しなのか?ボタンなのか?ナビゲーションなのか?などのコンテンツの役割を表すための属性です。

<div role="button">サンプル</div>

divは原則的に何も意味を持たないタグですが、role="button"を付与することでそのdivはボタンとしての役割を持つようになり、スクリーンリーダー(音声ブラウザ)などの支援技術はそれを「ボタン」と認識することができます。

僕たちはWebサイトのコンテンツを見ただけで「これはタブだな」とか「これは見出しだな」と判断することができますが、音声でインターネットしている方々には理解が難しい場合もあります。そういった方々に「これは◯◯です」と伝えるための属性だと覚えておきます。

ただし、HTMLタグには「暗黙のARIAセマンティック」が適用されているものがあります。例えばh1h6にはrole="heading"が既に適用されていますし、buttonにはrole="button"が既に適応されています。それらを重複して指定することは無意味です。(ただし、mainなどレガシーブラウザが対応していないようなタグはレガシーブラウザでは役割を認識できないので重複してrole="main"を付与させるケースもあります)

<!-- 重複してつける意味がない -->
<h2 role="heading"></h2>
<nav role="navigation"></nav>

<!-- IE11でも認識させたい場合は重複しても仕方がない -->
<main role="main"></main>

あくまでもHTMLタグを正しく使ってセマンティックにマークアップした上で他に役割を知らせたいことがあったらrole属性を使いましょう。HTMLをどうマークアップするか?は「スクリーンリーダーでどう読み上げられたいか?」を意識すれば迷うことはないような気がします。

<!-- SVG要素を画像として認識させたい -->
<svg role="img"></svg>

<!-- a要素をタブと認識させたい -->
<a role="tab" href="#"></main>

<!-- バリデーションなどで警告として認識させたい -->
<p role="alert">電話番号は必須です</p>

また、暗黙のARIAセマンティックを上書きする際は気をつけましょう。重要な役割を失ってしまう可能性があります。

<!-- 見出しとしての意味が消えてしまうのでよくない -->
<h3 role="button" tabindex="1"></h3>

aria属性

表示されているor隠れているといったコンテンツの状態明示や、どの箇所から参照されているか?などの特性明示を支援技術ユーザーに対して行うことができます。

classの付け替えで状態管理を行っているアコーディオンは目で見てそれが開いているかor閉じているかを判断することは容易ですが、支援技術はそれがどうなっているかを知ることができず、支援技術ユーザーに「アコーディオンが現在開いているか閉じているか」といった状態を伝えることはできません。

そこでaria属性で状態を明示すれば支援技術ユーザーにどうなっているかを伝えることができるようになります。

【スクリーンショット】MacのVoiceOverでしっかりと「折りたたまれた」と読み上げられています。

サンプルのアコーディオンをVoiceOverで読み上げた例ですが、しっかりと「折り畳まれました」と伝えられていますよね?

また、読み上げに含まれている「タブを開閉する」は実際には実際には表示されていないテキストです。これはaria-labelという特性明示のaria属性を使い、支援技術ユーザーに対してのみ読み上げを行っています。

<span class="my-accordion__tabicon" aria-label="タブを開閉する"></span>

このようにWAI-ARIAを用いてコンテンツ内の役割や特性、状態を明示することで、支援技術はWebページ内の構成や文脈、動きなどを理解することが可能になり、支援技術を利用しているユーザーに的確な情報を与えることができるようになります。

また、CSS設計においてもDOMの状態管理を名前のバラつきを起こしにくく、セマンティックに行うことができるようになるのもWAI-ARIAを用いることのメリットの一つですね。

/**
 * こう書いても悪くはないけれど
 */
.burger-button.is-active {
  /* ハンバーガーメニューが開いているときのハンバーガーボタンのスタイル */
}

/**
 * aria属性で指定したほうが名前のバラつきが起こりにくく標準化された属性なのでよりセマンティックに
 */
.burger-button[aria-expanded="true"] {
  /* ハンバーガーメニューが開いているときのハンバーガーボタンのスタイル */
}

■参考文献:
WAI-ARIA 実装の5つのルール | Accessible & Usable
WAI-ARIAを活用したフロントエンド実装 | 第1回 role属性、aria属性の基礎知識 | CodeGrid

アコーディオンの骨組み

まず、HTMLでアコーディオンの骨組みを作ってみます。僕は以下のような形で組んでみました。

<div class="my-accordion js-accordion">
  <section class="my-accordion__item">
    <h2 class="my-accordion__headline">
      <button type="button" class="my-accordion__tab js-accordion-tab">
        見出し
        <span class="my-accordion__tabicon"></span>
      </button>
    </h2>
    <div class="my-accordion__panel js-accordion-panel">
      <div class="my-accordion__panel-content">
        文章
      </div>
    </div>
  </section>
  
  ...
</div>

解説

  • 見出し+テキストのセクション構造にしています。これによってスクリーンリーダーの「見出しジャンプ機能」 を利用することができるようになり、支援技術を利用しているユーザーは見出しを拾い読みしてWebページのアウトラインをざっと把握することができるようになります。
  • スクリーンリーダーやキーボードのTab操作で選択ができるようにアコーディオンを開閉するタブは必ずabuttonで実装しましょう。divで実装したり、見出しそのものにクリックイベントを付けるのはアクセシブルではありません。(理由は前回の記事を参照)
  • jQueryのslideToggle的なアニメーションを用いたい場合はパネルのdivの中にコンテナのdivを追加しておいたほうがいいです。コンテナが無いと高さの再取得が辛くなってきます(僕がJS弱者なだけかもしれない)。slideToggleのためだけにjQuery使いたくなるのは僕だけでしょうか…。
  • スタイルを指定するためのclassとJSで参照するclassは分けています。今回の場合だと気にならないかもしれませんが、CSSは適用したいがJSの定義は不要、またはその逆ってパターンが起こった際に困ります。

アコーディオンを開閉するタブにWAI-ARIAを導入してみる

骨組みが終わったので早速WAI-ARIAを用いてアコーディオンのパーツに特性や状態を明示してみました。僕は以下のような形で属性を付与してみましたがいかがでしょうか?

<h2 class="my-accordion__headline">
 <button type="button" 
	 id="accordion-tab-number-1" 
	 class="my-accordion__tab js-accordion-tab" 
	 aria-expanded="false" 
	 aria-controls="accordion-panel-number-1">
  見出し
  <span class="my-accordion__tabicon" aria-label="タブを開閉する"></span>
 </button>
</h2>

解説

  • タブに連番を振ったidを付与する。アコーディオンのパネルに付与するaria-labelledby属性と紐付けを行うために必要です。
  • アコーディオンが閉じている(false)か開いている(true)かを伝えるためにタブにaria-expanded属性を付与します。タブの状態管理に用いたいので今回はタブに付与させることにしました。
  • パネルがどの要素から操作されるかを指定するためにタブにaria-controls属性を付与します。操作をされるパネルにaria-controlsの属性値と同じidを指定することで関連付けをします。
  • 「ボタン」と読ませるためにbuttonタグを利用。暗黙のARIAセマンティクスを尊重します。role属性は付与しません。アコーディオンの"タブ"だからrole="tab"を付与したら?と思うけどこれに関しては後述。汎用的なボタンなのでtype="button"を付与します。
  • タブ内のアイコン(.my-accordion__tabicon)に意味を持たせるためにaria-label属性を指定してその中に代替テキストを含めます。

注意点

  • aria-expanded属性は開閉している要素自身につけるのではなく、それを制御している要素もしくは開閉している要素の親要素につけること。今回の例だと開閉しているパネル(div.my-accordion__panel)に付与するのではなく、制御しているタブ(button.my-accordion__tab)もしくはパネルの親要素(section.my-accordion__item)に付与します。

■参考記事:aria-expandedのよくある間違い / masuP.net

レビューを受けて

1. aria-label="タブを開閉する"はいらないかも

このボタンが開閉できること自体と、開閉の状態はどちらもaria-expanded属性によってすでに担保されているため、aria-label="タブを開閉する"は冗長になってしまうとのことです。

ハンバーガーメニューのアイコンにaria-label="メニューを開閉する"と書くのは冗長なので「メニュー」でOKという理由と同じだそう。

<button type="button" 
	id="js-menu-toggle" 
	class="menu-toggle" 
	aria-controls="global-nav" 
	aria-expanded="false" 
	aria-label="メニュー">
  <span class="menu-toggle__line"></span>
  <span class="menu-toggle__line"></span>
  <span class="menu-toggle__line"></span>
</button>

ハンバーガーアイコンのマークアップはこんな感じでOKだそうです。

2. 暗黙的なroleのない要素にaria-label属性をつけるのは非推奨

明示的ないし暗黙的なroleが存在しない要素にaria-label属性を付与するのは仕様違反ではないが非推奨とのこと。支援技術や組み合わせによっては読み上げられなかったりするそうです

もしもspandivにアイコンフォントやCSSアイコンを表現するのであれば、role="img"を併記するとaria-labelも確実に読まれるようになるそうです。

<span class="my-accordion__tabicon" role="img" aria-label="タブを開閉する"></span>

付け加えるとこういった場合はvisually-hiddenクラスで画面から隠すアプローチの方が良さそうです。

.visually-hidden {
  /**
   * a11y-css-resetから引用
   * https://github.com/mike-engel/a11y-css-reset
  **/
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

修正後

<h2 class="my-accordion__headline">
 <button type="button" 
	 class="my-accordion__tab js-accordion-tab" 
	 aria-expanded="false" 
	 aria-controls="accordion-panel-number-1">
  見出し
  <span class="my-accordion__tabicon"></span>
 </button>
</h2>

アコーディオンのパネルにWAI-ARIAを導入してみる

続いてパネルにWAI-ARIAを付与していきます。

<div id="accordion-panel-number-1" 
     class="my-accordion__panel js-accordion-panel" 
     aria-hidden="true" 
     aria-labelledby="accordion-tab-number-1">
  <div class="my-accordion__panel-content">
    パネルのコンテンツ
  </div>
</div>

解説

  • パネルを制御しているタブのaria-controlsの属性値と同じものをidに付与します。これで影響を与える側と影響を受ける側を紐付けることが可能になりました。
  • aria-hidden="true"を指定することで支援技術の読み上げ対象から除外します。
  • aria-labelledby属性を利用してパネルのラベリングを行います。ラベルとなるテキストは制御しているタブの文言なのでaria-labelledbyの属性値に紐付いているタブのidの属性値を含めます。

■参考記事:ARIA ラベルと関係性 | Web | Google Developers

レビューを受けて

1. aria-hidden="true"と併せてdisplay: noneもしくはvisibility: hiddenを指定する

aria-hidden="true"は支援技術の読み上げからは除外されますが、実態は存在しているため中身にリンクやボタンなどのフォーカスの当たる要素が存在するとタブフォーカスを受け取ってしまうとのこと。aria-hidden="true"と合わせて display: noneもしくはvisibility: hiddenを指定して非表示にする ようにしましょう。今回はJSでコンテンツの高さを見ている関係でvisibility: hiddenを選択しました。

.my-accordion__panel[aria-hidden="true"] {
  visibility: hidden;
}

visibility: hiddenは用途があまりないと言われがちですが、display: noneが利用しにくい箇所を読み上げ時にも隠す場合には積極的に取り入れていきたいプロパティだなと。

2. aria-labelledby属性は不要

パネルのaria-labelledby属性は不要という指摘をいただきました。理由としては

  • マークアップ的に直上に見出しが存在している
  • roleがあるわけでもなくラベルが求められていることもない

とのこと。

修正後

<div id="accordion-panel-number-1" 
     class="my-accordion__panel js-accordion-panel" 
     aria-hidden="true">
  <div class="my-accordion__panel-content">
    パネルのコンテンツ
  </div>
</div>

アコーディオンのHTMLが完成

ざっとWAI-ARIAを付与して作られたアコーディオンのHTMLがこちらです。めっちゃ大変。

<div class="my-accordion js-accordion">
  <section class="my-accordion__item">
    <h2 class="my-accordion__headline">
      <button type="button" 
	      id="accordion-tab-number-1" 
	      class="my-accordion__tab js-accordion-tab" 
	      aria-expanded="false" 
	      aria-controls="accordion-panel-number-1">
       見出し
       <span class="my-accordion__tabicon" aria-label="タブを開閉する"></span>       
      </button>
    </h2>
    <div id="accordion-panel-number-1" 
	 class="my-accordion__panel js-accordion-panel" 
	 aria-hidden="true" 
	 aria-labelledby="accordion-tab-number-1">
      <div class="my-accordion__panel-content">
        文章
      </div>
    </div>
  </section>
  
  ...
</div>

ここからCSSとJSを書かないといけないんですねー…。

レビューを受けての修正後

<div class="my-accordion js-accordion">
  <section class="my-accordion__item">
    <h2 class="my-accordion__headline">
      <button type="button" 
	      class="my-accordion__tab js-accordion-tab" 
	      aria-expanded="false" 
	      aria-controls="accordion-panel-number-1">
       見出し
       <span class="my-accordion__tabicon"></span>       
      </button>
    </h2>
    <div id="accordion-panel-number-1" 
	 class="my-accordion__panel js-accordion-panel" 
	 aria-hidden="true">
      <div class="my-accordion__panel-content">
        文章
      </div>
    </div>
  </section>
  
  ...
</div>

role="tab"は付与しなくていいの?

先程少し触れましたがアコーディオン"タブ"なのだからタブ箇所にrole="tab"とパネルにrole="tabpanel"、そしてアコーディオンコンポーネントのルートにrole="tablist"を付与したほうがいいのでは?と思いましたが、どうやら現在は付ける必要は無さそうです。

というのも、現在のW3Cのドキュメントを読むと、以前はrole="tab"関連の記述があったにも関わらず、現在はアコーディオンのrole="tab"関連は軒並み削除されているからです。

アクセシビリティが意識されているBootstrapのコンポーネントでもアコーディオンにrole="tab"は付与されていませんでしたからね…ここらへんはアクセシビリティ有識者に伺いたいところです。

CSSとJSを書いていく

僕なりの注意点は以下です。

CSS

  • Tab移動時にタブにフォーカスが当たっていることが確認できるようにする。今回は:focus-visibleのPolyfillを導入してキーボード操作"以外"でフォーカスが当たったときのoutlineを消しています。
  • タブの見出しが改行してもいいようにCSSを指定しましょう。固定値のheightは使いません。

JS

  • パネルが開いた時→タブのaria-expandedtrueに、パネルのaria-hiddenfalse(もしくはaria-hidden属性自体を取り除く)に指定しましょう。
  • パネルが閉じた時→タブのaria-expandedfalseに、パネルのaria-hiddentrueに指定しましょう。
  • resizeイベントなりを使ってパネルの高さに変動があった時の高さの再計算を行いましょう。heightが固定されるので改行などが生じるとテキストが途切れてしまいます。ちなみにパネルにJSでheight(サンプルだとmax-heightですが)を指定している場合は``height: autoだとtransition`でアニメーションできないからです。
  • 「開くパネルは一つだけ(一つ開いたら他は閉じる)」か「複数のパネルを開ける」かは設定でオンオフできるようにしておくと良いと思います。

ちなみにサンプルのJSはES6なのでそのままではIEでは動かないです。トランスパイルなどしましょう。

おわりに

ただ開閉できるアコーディオンを作るのは容易いけど、アクセシビリティを意識して作ると見るところが多くなって非常に大変だなと思った。アクセシビリティを意識してWeb制作に取り組んでいる方々はすごいと思います(語彙力不足)。

記事の内容とかサンプルとかツッコミどころ多いかもしれませんが、指摘・コメントはDiscussionにて頂ければ嬉しいです。

それでは。

この記事に贈られたバッジ