4
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 20

【アクセシビリティ】アクセシビリティを意識したスピンボタンの作り方

Last updated at Posted at 2023-12-19

はじめに

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

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

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

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

アクセシビリティを意識したスピンボタンの仕様

⚪︎ スピンボタンとは?

スピンボタンは、入力する値を一連 or 範囲の離散的な値に限定するコンポーネントです。
例えば、アラームを設定するためのコンポーネントは、スピンボタンを使用して、時間の分数として0から59までの数を選択できるものです。

スピンボタンには、現在の値を表示するテキストフィールド、増加ボタン、減少ボタンの3つの要素があります。
テキストフィールドは、唯一フォーカス可能な要素で、増加と減少の機能は矢印キーを介してキーボードからアクセスできます。
テキストフィールドではユーザーが値を直接編集することもできます。

また、スピンボタンで入力する範囲が大きい場合、スピンボタンは小さなステップと大きなステップの両方で値を変更できるようにする場合があります。

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

  • ↑キー
    • 値を増加させる
  • ↓キー
    • 値を減少させる
  • Homeキー
    • スピンボタンに最小値がある場合、その最小値に設定する
  • Endキー
    • スピンボタンに最大値がある場合、その最大値に設定する
  • Page Upキー
    • 上矢印よりも大きいステップで値を増加させる
  • Page Downキー
    • 上矢印よりも大きいステップで値を減少させる
  • スピンボタンのテキストフィールドが値の直接編集を許可する場合、以下のキーもサポートされる
    • デバイスプラットフォームに適した標準の単行テキスト編集キー
    • テキストボックスに文字を入力する
      • 値の一部として特定の文字のみを許可し、他の文字の入力を防ぐ

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

  • スピンボタンとして機能するフォーカス可能な要素で、テキスト入力をサポートする要素には role="spinbutton" を設定する
  • スピンボタン要素には、スピンボタンの現在値を表す値を aria-valuenow を設定する
  • スピンボタン要素には、最小値が知られている場合、スピンボタンの最小許容値を表す値を aria-valuemin を設定する
  • スピンボタン要素には、最大値が知られている場合、スピンボタンの最大許容値を表す値を aria-valuemax を設定する
  • aria-valuenow の値がユーザーフレンドリーでない場合、aria-valuetext の値はスピンボタンの値を理解しやすくする文字列を設定する
  • スピンボタンに可視ラベルがある場合、スピンボタン要素上の aria-labelledby で参照される
    • そうでない場合、スピンボタン要素に aria-label でラベルを提供する
  • 値が許容範囲外の場合、スピンボタン要素に aria-invalid="true" を設定する
    • ほとんどの実装では無効な値の入力を防ぐことに注意すること
    • ただ、すべての無効な入力をブロックすることが実用的でない場合もある

アクセシビリティを意識したスピンボタンの完成形

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

アクセシビリティを意識したスピンボタンの作り方

1. HTMLを実装する

sample.html
<div class="container">
  <label id="number">Number</label>
  <input
    id="spinButton"
    type="number"
    aria-labeledby="number"
    min="0"
    aria-valuemin="0"
    max="100"
    aria-valuemax="100"
    placeholder="value"
  />
</div>

2. CSSを実装する

sample.css
body {
  background-color: #212529;
  color: #fff;
  display: grid;
  height: calc(100vh - 40px);
  margin: 0;
  padding: 20px 0;
  place-items: center;
  width: 100vw;
}

.container {
  background-color: rgb(128 128 128 / .3);
  border-radius: 24px;
  display: grid;
  padding: 16px;
  position: relative;
  min-width: 150px;
  background-blend-mode: luminosity;
  backdrop-filter: blur(15px);
  max-width: 300px;
  width: 100%;
  &::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 {
  font-size: 14px;
}

input {
  border: none;
  border-radius: 8px;
  background: linear-gradient(0deg, rgba(0, 0, 0, .1) 0%, rgba(0, 0, 0, 0.10) 100%), rgba(0, 0, 0, .5);
  background-blend-mode: luminosity, color-burn;
  box-shadow: 1px 1.5px 4px 0px rgba(0, 0, 0, 0.10) inset, 1px 1.5px 4px 0px rgba(0, 0, 0, 0.08) inset, 0px -0.5px 1px 0px rgba(255, 255, 255, 0.25) inset, 0px -0.5px 1px 0px rgba(255, 255, 255, 0.30) inset;
  color: #ffffff;
  font-size: 16px;
  padding: 8px 12px;
  margin-top: 8px;
}

3. JavaScriptを実装する

sample.js
document.addEventListener('DOMContentLoaded', () => {
  const spinButton = document.getElementById('spinButton');
  spinButton.addEventListener('input', (e) => {
    updatespinButton(e.target);
  });
  
  function updatespinButton(spinbutton) {
    const min = Number(spinbutton.min);
    const max = Number(spinbutton.max);
    const value = Number(spinbutton.value);
    spinbutton.ariaValueNow = value;    
    if (min > value || value > max) {
      spinbutton.ariaInvalid = true;
    } else {
      spinbutton.removeAttribute("aria-invalid");
    }    
  }
});

まとめ

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

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

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


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

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

4
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
4
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?