状態遷移時にアニメーションを伴うUIのアクセシビリティ周りの実装について

ディスクロージャーの開閉時やモーダルダイアログが表示される瞬間など、あらゆるUIは状態遷移のたびにアニメーションを伴う。にも関わらず、アクセシブルなUIを実装するための手法について書かれた文献で、アニメーションを伴う状態遷移時におけるWAI-ARIAの利用方法というようなテーマが取り上げられているものは見たことがない。UIにおけるアニメーションの役割を踏まえて、それをどのようにして実装すべきだと考えているかについて述べたい。

結論としては、セマンティクス上はアニメーションの存在を意識させないように実装すべきである。多くの場合、アニメーションはUIの状態遷移を視覚的に表現するために存在する。例えばディスクロージャーにおいては、閉じた状態と開いた状態のを擬似的にアニメーションによって表現することで、ユーザーが状態変化前後のビュー(View)の繋がりを理解する手がかりになる。これは視覚を用いてGUIを操作するユーザーのための実装だ。

対して、スクリーンリーダーなどの支援技術を通してウェブサイトを利用するユーザーにとってはこれらのアニメーションに意味はない。UIの状態が変化したときには、音声読み上げなどの方法によって随時通知されるというインターフェイスになっているからだ。そのため、ユーザーの動作を起点とした状態遷移時には、アニメーションを無視して即座に状態変化後のセマンティクスに変更されることが望ましい。でなければ動作と状態変化の関係性が結びつかなくなってしまう可能性があるからだ。

前述の例として取り上げたディスクロージャーを実装に落とし込みながら、より具体的に説明する。次のようなマークアップを基に実装することにする。

<section>
  <h2>
    <button type="button" class="trigger" aria-expanded="true">Content</button>
  </h2>
  <div class="body">
    <p>Lorem ipsum, dolor sit amet consectetur <a href="#">some link</a> adipisicing elit.</p>
    <p>Magni, <button type="button">some button</button> quod minima? Harum, consequatur esse?</p>
  </div>
</section>

.triggerをクリックすると.bodyが開閉する。初期状態では開いているという仕様。aria-controlsは一部のUAでしか実装されていないことに加えて、IDでの指定が運用上難しいため利用しないことが個人的に多い。

その前提の上で、ディスクロージャーを閉じるときに必要な処理は次のようになる。

  • .bodyをスライドアップするアニメーションを開始する
  • .triggeraria-expanded="false"を指定する
  • .bodyaria-hidden="true"を指定する
  • .bodyの子孫要素でTabbable(Tabキーでフォーカスできる)なものにtabindex="-1"を指定する

これによってアニメーションが開始すると同時に、セマンティクス上は閉じている状態になる。アニメーションの完了後もその状態は変化しない。ユーザーの動作後に支援技術には閉じていることが即座に通知されるということだ。

また、タブキーによってフォーカスを移動させるユーザー(支援技術及び一般ユーザー)のために、これから閉じようとしているコンテンツにはフォーカスさせないようにする。もちろん、アニメーションが終了して完全に閉じられた後も同様だ(終了時の.bodydisplay: nonevisibility: hiddenになる場合は不要)。

ディスクロージャーを開くときは先ほどの逆になる。

  • .bodyをスライドダウンするアニメーションを開始する
  • .triggeraria-expanded="true"を指定する
  • .bodyからaria-hidden="true"を取り除く
  • .bodyの子孫要素でTabbableなものからtabindex="-1"を取り除く

コードとしては次のようになるイメージだ。外部ライブラリとしてVelocity.jstabbableを利用している。実際に動くデモも用意した。

import Velocity from 'velocity-animate'
import tabbable from 'tabbable'

const triggerEl = document.querySelector('.trigger')
const bodyEl = document.querySelector('.body')
const tabbableEls = tabbable(bodyEl)
let isExpanded = true

const open = () => {
  Velocity(bodyEl, 'slideDown')

  tabbableEls.forEach(el => {
    el.removeAttribute('tabindex')
  })

  bodyEl.removeAttribute('aria-hidden')
  triggerEl.setAttribute('aria-expanded', 'true')
}

const close = () => {
  Velocity(bodyEl, 'slideUp')

  tabbableEls.forEach(el => {
    el.setAttribute('tabindex', -1)
  })

  bodyEl.setAttribute('aria-hidden', 'true')
  triggerEl.setAttribute('aria-expanded', 'false')
}

const toggle = () => {
  if (isExpanded) {
    close()
  } else {
    open()
  }

  isExpanded = !isExpanded
}

triggerEl.addEventListener('click', toggle)

モーダルダイアログなどの実装もこれと同じ考え方になる。間の状態を視覚的に表現するためのアニメーションは、支援技術及びキーボード操作をするユーザーにとっては存在しないように実装する。慣れれば難しくはない。


UIのユーザビリティを向上させるためにアニメーションは重要な要素だ。しかしそれはある特定のユーザーに対する個別最適化であるが故に、一部のユーザーにとっては使えない要因となってしまう危険性も孕んでいる。アクセシブルな実装が担保できた上で個別最適化を考えることはもっともであるが、その間口を広げる工夫をすることで、より多くのウェブサイトをアクセシブルにするということに前向きに取り組んでいけるはずだ。


この記事は『ライブラリなしで実装する定番UI - ドロワーナビの基本』に触発されて書かれました。素晴らしい記事を届けていただき、ありがとうございます。