既存 Web アプリケーションのアクセシビリティを向上させるときによくあるヤツと対応方針

アクセシビリティ向上メモ

最初から考慮されているのが一番ですが、そうでもなかったプロダクトに手を入れるときのあるあるを記録します。既存プロダクトはモノが出来上がってしまっているため根治的なリファクタリングよりも基本的には省力で済ませたいので、今回書いた対応方法も完璧よりは省力路線です。

HTML やらセマンティクスに関する知識はそれなりにあるつもりでしたが、AT やマシンリーダビリティを念頭に勉強し直すと自分で作ったものも至らなかったなと振り返らざるを得ない今日この頃。初期設計のときの考慮範囲が拡がったように思うので善きかなです。

各項目について、もっと良い解法や誤り等あれば twitter とかでご指摘ください。

1. 画像に alt 属性がない場合

付けましょう。付けます。はい。

昔から基本とも言うべき、HTML/CSS 書きとしては耳にたこができるほど言われてきたことなので今更感もありますが Web アプリケーション開発の現場では alt がないことも珍しくありません。マークアップ業を専門的にやったことがなければ、さもありなんといった所です。

近隣でみた事例では、更新用ネイティブアプリの内部用途で alt の属性値が謎の文字列で汚染されているケースもありました。data-* でやれ案件です。はてなブログも中々ファンキーな alt が設定されています。

場合によっては画像の alt 属性値を空にする

スクリーンリーダーで実際に読ませて違和感がないように付けるというのは、単に alt 付けようぜ!とは別問題という話もあります。アイコン画像に兄弟関係のテキストノードと同じ内容の alt を設定するのはおそらく冗長なので、属性値を空にすべきです。

<img> のあとにキャプションがある場合も、alt とキャプション相当の要素が同じテキストになるくらいなら、alt を空にしても良い気がします。これもアイコン画像 with テキストの場合と同様に、丁寧に書いたところで2重に読み上げられるだけなので冗長です。

<figure>
  <img src="/img/foo.png" alt />
  <figcaption>foo!foo!</figcaption>
</figure>

もちろん alt とキャプション相当の要素が別のテキストで成立するのであれば、別々に提示すれば良いでしょう。

UGC(User Generated Contents)における alt の制御方針

いわゆる UGC(User Generated Contents) であったり単にブログであったり、一般のユーザーが使用するツールであれば次のいずれかを徹底するのが安定するように思います。

  • キャプション情報の入力を推奨して alt の属性値に使う
  • キャプション情報の入力を推奨して alt の属性値は空にして <figcaption> を添える

前職で開発していた CMS は alt とキャプションを別々に設定できる分、自由度が高すぎて構築側がコントロールしないと適切なコンテンツが生成されないだろうなと思いを馳せるところです。

SVG は中に title 要素で入れる

SVG だと alt って感じではなく中のテキスト情報をアテにするスタイルなので、このようにします。

<svg  aria-labelledby="title123">
  <title id="title123">alternative text</title>
  <use xlink:href="/img/icons.svg" role="link"></use>
</svg>

こんな感じでしょうか。aria-label はサポート状況にムラがあるようですが、SVG の <title> は割と安定して読まれそうな雰囲気があります。日本語スクリーンリーダーにおけるインラインSVG対応状況のツラい話 - 週刊SVGを参考にしました。

2. <a> であるべきが <span> とか <div>

リンク(画面遷移アクション)は <a> だよね、とは言ってもやんごとなき事情によってそうなってはいないこともあります。

onClick で動的に移動先を決めて遷移する場合も <a> を使う

JavaScript で遷移先を決めるから、<a>href 属性が邪魔になるといった理由で <span onClick={this.onClick} /> のように済ますケースをたまに見かけることがあります。その場合でも event.preventDefault() でキャンセルしてでも最低限 <a> の体裁は保つべきです。

onClick(routeName, e) {
  e.preventDefault();
  const url = this.getNextUrl();
  location.href = url;
}

しかし、こういうことをやると遷移先の URL が事前に分かりませんし、もう一手間かけないと新しいウインドウ/タブで開く系のアクションも殺すことになってしまいます。実装的に悩ましいこともあると思いますが、原則としては onClick に頼らず <a href="http.....">link</a> を徹底するべきでしょう。

リンクの中に別のリンクがある場合は <object> が使える

この <a> in <a> のパターンは、HTML/CSS の知識がない同僚から良く出てくる案件ですが一応、次のようなハック対応が存在します。

// ダメな例1
<a href="/foo">foo<span onClick={this.linkToBar}>bar</span>baz</a>

// ダメな例2
<a href="/foo">foo<a href="/bar">bar</a>baz</a>

// ハック例
<a href="/foo">foo<object><a href="/bar">bar</a></object>baz</a>

元ネタは Dirty Tricks From The Dark Corners Of Front-End // Speaker Deck の冒頭にあります。 <object> による魔術ですが、バリデーター的にはちゃんと Valid です。

3. <button> であるべきが <span> とか <div> とか <a>

ボタン(画面遷移以外のアクション)は <button> だよね、とは言ってもやんごとなき事情によってそうなってはいないこともあります。

ただのうっかりさんを防止するための周知は必要

実装者が知識をもっていない or ガイドラインが定められていない場合、ボタン UI が <span><div><a> で代替されることは前述したリンクの話よりも自然な流れです。

ボタンを <button> としてマークアップすることでキーボード操作の恩恵をまとめて受けられたり、マークアップとして正しい意味づけができたりといったメリットについて、一度は周知徹底しなければならないでしょう。

<button> がフローコンテンツを内包できないが故なら、代わりに属性で説明する

ボタン UI が複雑化して、うっかり <button> 相当の要素内にフローコンテンツ...たとえば <p><div> が入ってしまった場合は、<div> のまま押し切らざるをえない時もあります。やむを得ないときは、次のように属性でボタンであることを説明できます。

<div role="button" aria-label="foo" title="foo" tabindex="0">
  <!-- フローコンテンツを含む複雑な何か -->
</div>

role="button" で要素がボタンであることを説明し、tabindex="0" でフォーカス可能にします。ラベルについては aria-label だけだと PC-Talker あたりが対応していないような情報を見かけたので title も振ります。PC-Talker にせよ JAWS にせよ、予算おとさないと買えない高額商品...。

フローコンテンツとフレージングコンテンツを間違えて書いて変な索引してたので修正しました

4. 出たり消えたりするサイドバー

Web アプリケーションやモバイルサイトではよくあるのが、出たり消えたりするスライド式のサイドバーです。隠れている状態で読み上げ対象になってもらっては困るので、確実に非表示扱いになっていなければなりません。

スクリーンリーダーも visibility-hiddendisplay: none などのスタイルが適用されている要素は読み上げませんが、加えて aria-hidden で表示状態を明示できると完璧です。

[aria-hidden="true"]visibility: hidden の連動

Supported States and Properties | Accessible Rich Internet Applications (WAI-ARIA) 1.0 でも例が示されていますが、aria-hidden という表示状態を示す属性とスタイルを関連づける方法があります。

[aria-hidden="true"] { visibility: hidden; } /* 使い勝手に応じて、display: none でも良さそう */

JavaScript で .is-hidden { visibility: hidden } のようなクラスを付け外しするよりは W3C の標準仕様に寄り添っているため、実装の妥当性が高いです。

トランジションの適用と aria-hidden の更新タイミングは整理が必要

ところがサイドバーというものは大抵、トランジションまたはアニメーションが付いてきます。このようにトランジションを伴う UI を、前述した [aria-hidden="true"] { visibility: hidden; } に基づき aria-hidden の付け外しだけで表示/非表示を切り替えるとトランジションが損なわれます。

  • (React風に言うと) 本来は state の変更にともなう render で aria-hidden を即時適用してしまいたい
  • スタイルに紐付いた aria-hidden="true" を即時適用すると、トランジションより先に非表示になる
  • トランジション用の className と、aria-hidden は別々に管理しなければならない
  • 出すとき: aria-hidden="false" とトランジション用の className を同時に適用
  • 隠すとき: トランジション用の className を適用して transitionEndaria-hidden="true" を適用

というような展開になります。

もしくは aria-hidden="true"visibility: hidden の紐付きをなくして、トランジション用の className に定義された @keyframes の最後に visibility: hidden を適用するような方法になると考えられます。

5. SPA の画面遷移時にページタイトルを読み上げさせる

SPA で画面遷移したときに、遷移先の新しいページタイトルを読み上げさせるのは Improving Single Page App Accessibility を参考に実装してみました。次の JavaScript は単純な例です。

const announcer = document.getElementById('announcer');

function _clearAnnouncer() {
  announcer.innerHTML = '';
  announceTimeout = null;
}

function _setAnnouncer(message) {
  announcer.setAttribute('aria-live', 'off');
  announcer.setAttribute('aria-live', 'assertive');
  announcer.innerHTML = message;
}

function setPageTitle(pageTitle) {

  document.title = pageTitle;

  _setAnnouncer(pageTitle);
  clearTimeout(announceTimeout);
  announceTimeout = setTimeout(_clearAnnouncer, 500);
}

aria-live を使って、画面遷移時に新しいページタイトルをスクリーンリーダーに強制的に読み上げさせるような実装になっています。

現場からは以上です

  • 普段から注意すべきポイントをガイドライン化しておくこと
  • WAI-ARIA に対応する追加工数をどうにかして見積もること

通常の Web サイトであれば前者のガイドライン遵守だけでも工数へのインパクトを抑えながら対応できるかもしれませんが、Web アプリケーションとなると複雑な UI で構成される箇所も多く WAI-ARIA についてはどうやっても追加工数でしかないので何らかの努力が必要です。

WAI-ARIA の工数を削減するために一番手っ取り早いのは、WAI-ARIA に対応している既存の UI コンポーネントを使うことです。React でも Angular でも海外産の有名なサードパーティコンポーネントは WAI-ARIA をサポートしていることは珍しくありません。コンポーネント(ないし jQuery プラグイン)の採用基準に WAI-ARIA 対応を入れるのは妥当な取り組みです。

既製品に頼れない UI のときは、がんばってスクラッチしましょう。はい。