ARIA-Barriers

標準でないチェックボックスはいろいろ難しい

2024年02月07日

今回は、ACTLabの代表、吹雪がお送りします。 私は弱視なので、マウスとスクリーンリーダーを組み合わせてウェブを閲覧しています。

みんな、標準のチェックボックスに不満を抱えている

普段からウェブサイトを閲覧している方であれば、以下のようなチェックボックスを一度は見たことがあるかと思います。 これは、HTMLのinput要素を用い、それ以上何の装飾や工夫もしていない、単純なチェックボックスです。 HTMLの標準要素なので、どのブラウザ、どのスクリーンリーダーでも支障なく利用することができるはずです。

<div class="bg-gray-200 my-8 py-4 px-4 flex items-center cursor-pointer">
  <label class="flex items-center">
    <input type="checkbox" class="mr-2">
    はい、見たことがあります
  </label>
</div>

しかし、世の中にはこの標準のチェックボックスを、「使いにくい」「アクセシビリティが低い」「そのまま使うべきでない」などという人がたくさんいます。 この記事を書いている私も、その一人だったりします。弱視の私には小さくて見えにくいのです。 様々な考えの人がいると思いますが、下記のようなことがよく言われます。

こういった理由で、「独自のチェックボックスを作りたい」という需要が生まれ、先人たちは様々なソリューションを生み出してきました。 そして、そこにはWAI-ARIAも無関係ではありません。

CSSで改造されるチェックボックス

たとえば、「opacity:0」(不透明度0=透明)を指定するなどの方法でチェックボックスを画面から隠してしまい、代わりにCSSを駆使して独自の描画を行う方法があります。 Javascriptによる動作が非表示になっているチェックボックスのON/OFFの切り替えのみであれば、この手の改造によるアクセシビリティ上の悪影響は小さい場合が多いです。

チェックボックスではないチェックボックス

クリック範囲の変更や細かな見た目の調整を行いたい場合などには、input要素に見切りをつけ、div要素やspan要素を用いてチェックボックスを作ってしまうという方法を選択されることがあります。 onClickイベントでチェックマークの有無を切り替えるということになるのですが、この方法で設置されるのは「見た目だけはチェックボックスっぽく見えるただの文字」にすぎません。 キーボードでの操作やスクリーンリーダーでの読み上げなど、様々な点で問題があります。

中途半端なWAI-ARIAの設定がもたらす結果

ここからが本題です。 WAI-ARIAの一つに、「role」という属性があります。 この属性を使うと、img要素で設置した画像でも、前節で出てきた「見た目だけはチェックボックスっぽく見えるただの文字」でも、果てはラジオボタンでさえも「チェックボックスである」と示すことができます。 要素に「role=”checkbox”」を設定することで、どんな要素を使って作ったチェックボックスであっても、スクリーンリーダーは「チェックボックス」と認識します。

では、この「role=”checkbox”」を設定すれば、スクリーンリーダーユーザーに対するアクセシビリティ対応をしたといえるでしょうか。 試しに、「div要素・アイコン・p要素を用いて作ったチェックボックス」にこの設定を施したものを下に配置しておきます。

<div class="bg-gray-200 my-8 py-4 px-4 flex items-center cursor-pointer" role="checkbox" data-checked="false" id="customCheckbox1">
  <span class="far fa-square mr-2"></span>
  <p>チェック欄</p>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const checkbox1 = document.getElementById('customCheckbox1');
    checkbox1.addEventListener('click', function() {
        const isChecked = this.getAttribute('data-checked') === 'true';
        if (isChecked) {
            this.setAttribute('data-checked', 'false');
            this.querySelector('span').classList.remove('fa-check-square');
            this.querySelector('span').classList.add('fa-square');
        } else {
            this.setAttribute('data-checked', 'true');
            this.querySelector('span').classList.remove('fa-square');
            this.querySelector('span').classList.add('fa-check-square');
        }
    });
});
</script>

上のチェックボックスっぽいものにマウスを当てると、NVDAは「チェックボックス チェックなし チェック欄」と読み上げます。 意図したとおり、チェックボックスとして認識されています。 これだけを聞くと、何の問題もなさそうに見えます。 せっかちな人ならば「対応完了。リリース!」と言ってしまっても不思議ではありません。

roleを設定しただけのチェックボックスは壊れている

下に、先程とほぼ同じチェックボックスがあります。 唯一の違いは、最初からチェック状態であるということです。

<div class="bg-gray-200 my-8 py-4 px-4 flex items-center cursor-pointer" role="checkbox" data-checked="true" id="customCheckbox2">
  <span class="far fa-check-square mr-2"></span>
  <p>チェック欄</p>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const checkbox2 = document.getElementById('customCheckbox2');
    checkbox2.addEventListener('click', function() {
        const isChecked = this.getAttribute('data-checked') === 'true';
        if (isChecked) {
            this.setAttribute('data-checked', 'false');
            this.querySelector('span').classList.remove('fa-check-square');
            this.querySelector('span').classList.add('fa-square');
        } else {
            this.setAttribute('data-checked', 'true');
            this.querySelector('span').classList.remove('fa-square');
            this.querySelector('span').classList.add('fa-check-square');
        }
    });
});
</script>

こちらにマウスを当てると、NVDAは「チェックボックス チェックなし チェック欄」と読み上げます。 チェック状態であるにもかかわらず、「チェックなし」と読み上げます。 これでは、正しく実装されたチェックボックスであるとは言えません。

この状態のチェックボックスは、下記のような問題を引き起こします。

ARIAを完全実装しても、まだまだ壊れている

MDNによるARIA: checkbox ロールの説明ページには、「role=”checkbox” を含む要素には、チェックボックスの状態を支援技術に公開するための aria-checked 属性も含める必要があります。」という解説があります。 前節までのものにaria-checked属性を適切に切り替える実装を追加して、下に設置しておきます。

<div class="bg-gray-200 my-8 py-4 px-4 flex items-center cursor-pointer" role="checkbox" aria-checked="false" id="customCheckbox3">
  <span class="far fa-square mr-2"></span>
  <p>チェック欄</p>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const checkbox3 = document.getElementById('customCheckbox3');
    checkbox3.addEventListener('click', function() {
        const isChecked = this.getAttribute('aria-checked') === 'true';
        if (isChecked) {
            this.setAttribute('aria-checked', 'false');
            this.querySelector('span').classList.remove('fa-check-square');
            this.querySelector('span').classList.add('fa-square');
        } else {
            this.setAttribute('aria-checked', 'true');
            this.querySelector('span').classList.remove('fa-square');
            this.querySelector('span').classList.add('fa-check-square');
        }
    });
});
</script>

これで、チェック状態に応じて適切に状態が読み上げられるようになりました。

しかし、上のチェックボックスはまだ壊れています。 まず、クリックして「チェック」「チェックなし」の状態を変更したときに読み上げが行われません。 また、TABキーで入力要素間を移動した際に、フォーカスされないという問題があります。 このままでは、フォームの入力体験として不便であるとともに、チェックボックスの存在に気づかないユーザーが出てしまうことになりかねません。

tabindexの指定をしても、まだまだまだ壊れている

Tabキーでフォーカス可能にするには、tabindexを指定することが有効です。 tabindex=0を指定すれば、TABキーでのフォーカスが可能になります。 そして、クリックしてチェック状態を切り替えた瞬間に、切り替わったことの音声ガイドもなされるようになります。

<div class="bg-gray-200 my-8 py-4 px-4 flex items-center cursor-pointer" role="checkbox" aria-checked="false" id="customCheckbox4" tabindex="0">
  <span class="far fa-square mr-2"></span>
  <p>チェック欄</p>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const checkbox4 = document.getElementById('customCheckbox4');
    checkbox4.addEventListener('click', function() {
        const isChecked = this.getAttribute('aria-checked') === 'true';
        if (isChecked) {
            this.setAttribute('aria-checked', 'false');
            this.querySelector('span').classList.remove('fa-check-square');
            this.querySelector('span').classList.add('fa-square');
        } else {
            this.setAttribute('aria-checked', 'true');
            this.querySelector('span').classList.remove('fa-square');
            this.querySelector('span').classList.add('fa-check-square');
        }
    });
});
</script>

tabindexの指定で状態切り替え時のガイドの有無が変化する理由について確かなことは分からないのですが、 インタラクティブな要素として認識され、何らかのイベントの発火対象になるか否かの違いが生じるのかなと思っています。

マウス操作するチェックボックスとしては、ここまでで本物と遜色ない実装になったはずです。 しかし、キーボードでの操作をするには、まだ不足があります。

キー操作への対応をして、一応完成

本来のチェックボックスであれば、TABキーでフォーカスを当てた後、Spaceキーによってチェック状態を切り替える操作を行うことができます。 しかし、前節の例では、この操作を正しく行うことができません。実装が不足しているからです。

ARIA role はあくまで支援技術向けの情報なので、これを付けることによって動作が加わることはありません。 そのため、ARIAをつかってフォーム部品を自作する場合には、マウス操作とキー操作の双方を漏れなくスクリプトで定義しておく必要があります。 前節の例に、keyupイベントを捕まえてチェック状態を切り替えるスクリプトを追加します。

<div class="bg-gray-200 my-8 py-4 px-4 flex items-center cursor-pointer" role="checkbox" aria-checked="false" id="customCheckbox5" tabindex="0">
  <span class="far fa-square mr-2"></span>
  <p>チェック欄</p>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const checkbox5 = document.getElementById('customCheckbox5');
    function toggleCheckbox(checkbox) {
        const isChecked = checkbox.getAttribute('aria-checked') === 'true';
        if (isChecked) {
            checkbox.setAttribute('aria-checked', 'false');
            checkbox.querySelector('span').classList.remove('fa-check-square');
            checkbox.querySelector('span').classList.add('fa-square');
        } else {
            checkbox.setAttribute('aria-checked', 'true');
            checkbox.querySelector('span').classList.remove('fa-square');
            checkbox.querySelector('span').classList.add('fa-check-square');
        }
    }
    checkbox5.addEventListener('click', function() {
        toggleCheckbox(this);
    });
    checkbox5.addEventListener('keydown', function(event) {
        if (event.key === ' ') {
            event.preventDefault();
        }
    });
    checkbox5.addEventListener('keyup', function(event) {
        if (event.key === ' ') {
            toggleCheckbox(this);
            event.stopPropagation();
        }
    });
});
</script>

この実装は ARIA Authoring Practices Guide (APG) を参考にしました。 keydownイベントでSpaceキーが押されたときにデフォルトの動作(ページのスクロール)を防ぐために event.preventDefault() を呼び出しています。 そして、keyupイベントでSpaceキーが離されたときにチェックボックスの状態を切り替えています。 Spaceキーを押し続けている間はチェックボックスの状態が切り替わらず、Spaceキーを離したときに初めて状態が切り替わります。

ここまで行えば、本来のチェックボックスとほぼ遜色のないものを設置できたはずです。 逆に言えば、ここまでに説明した事項のいずれか1つでも欠けているチェックボックスもどきは、壊れているということになります。

まとめ

スクリーンリーダー利用者への対応として、「適切なroleを設定すればよい」という認識は、ほとんどの場合不十分、もしくは誤っています。 各roleには細かな使い方のルールや同時に組み合わせて使用する必要のある別の属性が存在するものが多くあります。 そして、マウス・キーボードのそれぞれにおいて、洗練された操作体系が存在します。

ARIA role を使用するのであれば、これらを熟知して適切に実装し、キーボードでの操作やスクリーンリーダーでの読み上げを入念にテストする必要があります。 数多くの知識、それなりの実装時間、ブラウザやスクリーンリーダーを複数組み合わせたテスト環境など、多くのリソースを必要とします。 もしこれらが十分でないのならば、ARIA roleを使用するべきではありません。 これらがすべてそろった開発の現場は、かなりレアではないでしょうか。 ARIA role はそういったレアな現場のためのものであり、ほとんどの場合には使用を推奨できないどころか、非推奨です。

中途半端なARIAの使用は、何もしていないサイト以上に厄介な存在となりかねません。 それを示すために書いたのが、この記事です。 コンボボックスやテキストエリアなど他の要素と比較し、チェックボックスでできることや実装すべき事項は少ないほうです。 それでもこの分量になってしまいました。 標準要素のまま○○を変更するのは手間がかかる。とはよく言われることですが、この記事で示したことを考慮してARIAを用いた独自実装を行うことと比べれば、かかる手間はかなり小さいという場合が多いのではないでしょうか。

理解・時間・テストが不足したARIAの使用は有害であり、それを修正するために考慮すべき点が多数に上ることをこの記事から感じていただき、 標準のHTMLタグを用いた実装にこだわるという決意をしていただける方が一人でも増えることを願っています。