6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コンポーネントごとに考えるアクセシビリティAdvent Calendar 2023

Day 7

【アクセシビリティ】アクセシビリティを意識したチェックボックスの作り方

Last updated at Posted at 2023-12-06

はじめに

みなさんアクセシビリティを意識して開発できていますか?

必要なところにrole属性を記述したり、tabキーでフォーカスができるようにしたりなど、意識しないといけないことも多いです。
そのため、アクセシビリティを完璧にやろうとするのは一苦労です。

ただ、コンポーネントごとに区切って、アクセシビリティを理解しておけば、実装するタイミングに思い出しやすく、アクセシビリティも意識しやすいと思います。

そのため、この記事では「チェックボックス」に焦点を当てて、アクセシビリティを意識したチェックボックスの実装方法とチェックボックスで意識した方がいいアクセシビリティを解説しようと思います。

アクセシビリティを意識したチェックボックスの仕様

⚪︎ チェックボックスとは?

チェックボックスは、チェック済みチェック済みでない 状態を切り替えることができる要素です。
また、一部のチェックボックスでは、チェック済みチェック済みでない部分的にチェック済み 状態を切り替えることができます。

3つ以上の状態を切り替えることができるチェックボックスは、1つのチェックボックスがチェックボックスグループ全体を チェック済みチェック済みでない の状態を切り替え、チェックボックスグループの各要素は、個別に チェック済みチェック済みでない の状態を切り替えるものです。

3つ以上の状態を切り替えることができるチェックボックスは以下のような機能がが必要です。

  • チェックボックスグループの各要素が全部が チェック済み の場合、チェックボックスグループ全体のチェックボックスも チェック済み になる
  • チェックボックスグループの各要素の一部が チェック済み の場合、チェックボックスグループ全体のチェックボックスは、部分的にチェック済み になる
  • チェックボックスグループの各要素の全部が チェック済みでない の場合、チェックボックスグループ全体のチェックボックスは、チェック済みでない になる
  • チェックボックスグループ全体のチェックボックスを チェック済み にすると、チェックボックスグループの各要素が全部が チェック済み になる
  • チェックボックスグループ全体のチェックボックスを チェック済みでない にすると、チェックボックスグループの各要素が全部が チェック済みでない になる

⚪︎ キーボードインタラクション

チェックボックスにフォーカスがある時、Spaceキー を押すとチェックボックスの状態が変わる。

⚪︎ WAI-ARIA の役割、状態、プロパティ

  • チェックボックスには、role="checkbox" が設定されている
  • チェックボックスは以下のどれかで、アクセス可能なラベルを設定する
    • 要素内に含まれる可視テキストコンテンツ
    • チェックボックスの要素に設定された、aria-labelledby によって参照される可視ラベル
    • チェックボックスの要素に設定された、aria-label
  • チェック済みの時、aria-checked="true" を設定する
  • 部分的にチェック済みの場合は、aria-checked="mixed" を設定する

アクセシビリティを意識したチェックボックスの完成形

See the Pen Button Accessibility by でぐぅー | Qiita (@sp_degu) on CodePen.

アクセシビリティを意識したチェックボックスの作り方

1. HTMLを実装する

sample.html
<div class="checkbox-container toggle">
  <label><input type="checkbox" aria-checked="false"/>りんご 🍎</label>
  <label><input type="checkbox" aria-checked="false"/>なし 🍐</label>
  <label><input type="checkbox" aria-checked="false"/>バナナ 🍌</label>
  <label><input type="checkbox" aria-checked="false"/>スイカ 🍉</label>
</div>

<div class="checkbox-container tri">
  <label id="all"><input type="checkbox" aria-checked="false" aria-controls="cond1 cond2 cond3 cond4"/>全部チェックする</label>
  <div id="checkbox-group">
    <label><input type="checkbox" id="cond1" aria-checked="false"/>りんご 🍎</label>
    <label><input type="checkbox" id="cond2" aria-checked="false"/>バナナ 🍌</label>
    <label><input type="checkbox" id="cond3" aria-checked="false" />スイカ 🍉</label>  
  </div>
</div>

2. CSSを実装する

sample.css
/* 以下スタイル調整 */
body {
  align-items: center;
  background-color: #212529;
  color: #fff;
  display: flex;
  gap: 40px;
  height: calc(100vh - 40px);
  justify-content: center;
  margin: 0;
  padding: 24px 0;
  width: 100vw;
}

.checkbox-container {
  background-color: rgb(128 128 128 / .3);
  border-radius: 24px;
  display: grid;
  gap: 8px;
  padding: 16px;
  position: relative;
  min-width: 150px;
  background-blend-mode: luminosity;
  backdrop-filter: blur(15px);
  &::before {
    background: linear-gradient(135deg, rgb(255 255 255 / .4) 0, rgb(255 255 255 / 0) 40%, rgb(255 255 255 / 0) 60%, rgb(255 255 255 / .1) 100%);
    border: 1.4px solid transparent;
    border-radius: 24px;
    content: "";
    inset: 0;
    position: absolute;
    -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0) border-box;
    -webkit-mask-composite: destination-out;
    mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0) border-box;
    mask-composite: exclude;
    z-index: -1;
  }
}

label {
  cursor: pointer;
  padding: 4px;
  
  & > input[type="checkbox"] {
    cursor: pointer;
    margin-right: 8px;
    position: relative;
  }
  & > input[aria-checked="mixed"]::after {
    background-color: #212529;
    border-radius: 2px;
    content: "";
    display: block;
    height: 3px;
    position: absolute;
    width: 7px;
    top: 5px;
    left: 3px;
  }
}

#checkbox-group {
  padding-left: 24px;
  display: grid;
  gap: 8px;
}

3. JavaScriptを実装する

sample.js
 window.addEventListener('load', function () {
  let toggleCheckboxes = document.querySelectorAll('.toggle label input[type="checkbox"]');
  for (let i = 0; i < toggleCheckboxes.length; i++) {
    new ToggleCheckbox(toggleCheckboxes[i]);
  }
});

class ToggleCheckbox {
  constructor(domNode) {
    this.domNode = domNode;
    this.domNode.addEventListener('keydown', this.onKeydown.bind(this));
    this.domNode.addEventListener('click', this.onClick.bind(this));
  }
  
  toggleCheckbox() {
    if (this.domNode.getAttribute('aria-checked') === 'true') {
      this.domNode.setAttribute('aria-checked', 'false');
    } else {
      this.domNode.setAttribute('aria-checked', 'true');
    }
  }
  
  onKeydown(event) {
    if (event.key === ' ') {
      this.toggleCheckbox()
    }
  }
  
  onClick(event) {
    this.toggleCheckbox()
  }
  
}


class CheckboxMixed {
  constructor(domNode) {
    this.mixedNode = domNode.querySelector('.tri #all input[type="checkbox"]');
    this.checkboxNodes = domNode.querySelectorAll('.tri #checkbox-group input[type="checkbox"]');

    this.mixedNode.addEventListener('keydown', this.onMixedKeydown.bind(this));
    this.mixedNode.addEventListener('click', this.onMixedClick.bind(this));

    for (var i = 0; i < this.checkboxNodes.length; i++) {
      var checkboxNode = this.checkboxNodes[i];

      checkboxNode.addEventListener('click', this.onCheckboxClick.bind(this));
      checkboxNode.setAttribute('data-last-state', checkboxNode.checked);
    }

    this.updateMixed();
  }

  updateMixed() {
    var count = 0;

    for (var i = 0; i < this.checkboxNodes.length; i++) {
      if (this.checkboxNodes[i].checked) {
        count++;
      }
    }

    if (count === 0) {
      this.mixedNode.setAttribute('aria-checked', 'false');
      this.mixedNode.checked = false;
    } else {
      if (count === this.checkboxNodes.length) {
        this.mixedNode.setAttribute('aria-checked', 'true');
      this.mixedNode.checked = true;
      } else {
        this.mixedNode.setAttribute('aria-checked', 'mixed');
        this.updateCheckboxStates();
        this.mixedNode.checked = false;
      }
    }
  }

  updateCheckboxStates() {
    for (var i = 0; i < this.checkboxNodes.length; i++) {
      var checkboxNode = this.checkboxNodes[i];
      checkboxNode.setAttribute('data-last-state', checkboxNode.checked);
    }
  }

  anyLastChecked() {
    var count = 0;

    for (var i = 0; i < this.checkboxNodes.length; i++) {
      if (this.checkboxNodes[i].getAttribute('data-last-state') == 'true') {
        count++;
      }
    }

    return count > 0;
  }

  setCheckboxes(value) {
    for (var i = 0; i < this.checkboxNodes.length; i++) {
      var checkboxNode = this.checkboxNodes[i];

      switch (value) {
        case 'last':
          checkboxNode.checked =
            checkboxNode.getAttribute('data-last-state') === 'true';
          break;

        case 'true':
          checkboxNode.checked = true;
          break;

        default:
          checkboxNode.checked = false;
          break;
      }
    }
    this.updateMixed();
  }

  toggleMixed() {
    var state = this.mixedNode.getAttribute('aria-checked');

    if (state === 'false') {
      if (this.anyLastChecked()) {
        this.setCheckboxes('last');
      } else {
        this.setCheckboxes('true');
      }
    } else {
      if (state === 'mixed') {
        this.setCheckboxes('true');
      } else {
        this.setCheckboxes('false');
      }
    }

    this.updateMixed();
  }

  /* EVENT HANDLERS */

  // Prevent page scrolling on space down
  onMixedKeydown(event) {
    if (event.key === ' ') {
      event.preventDefault();
    }
  }

  onMixedClick() {
    this.toggleMixed();
  }

  onCheckboxClick(event) {
    event.currentTarget.setAttribute(
      'data-last-state',
      event.currentTarget.checked
    );
    this.updateMixed();
  }
}

// Initialize mixed checkboxes on the page
window.addEventListener('load', function () {
  let mixed = document.querySelectorAll('.tri');
  for (let i = 0; i < mixed.length; i++) {
    new CheckboxMixed(mixed[i]);
  }
});

まとめ

この記事では、「チェックボックス」に焦点を当てて、アクセシビリティを意識したチェックボックスの実装方法とチェックボックスで意識した方がいいアクセシビリティを解説しました。

ぜひこの記事をストックして、チェックボックスを実装する時にアクセシビリティについて思い出してもらえると嬉しいです。

Advent Calendar 2023では、他のコンポーネントにも焦点を当てて、アクセシビリティについても解説しているので、ぜひ購読していてください。


最後まで読んでくださってありがとうございます!

普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?