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

## はじめに
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390720/ef717c5c-a7c2-c4e2-2ab9-ef931274fa64.png)

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

必要なところに`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"` を設定する
  + ほとんどの実装では無効な値の入力を防ぐことに注意すること
  + ただ、すべての無効な入力をブロックすることが実用的でない場合もある

## アクセシビリティを意識したスピンボタンの完成形
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="zYeapEy" data-editable="true" data-user="sp_degu" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/sp_degu/pen/zYeapEy">
  Combobox Accessibility</a> by でぐぅー | Qiita (<a href="https://codepen.io/sp_degu">@sp_degu</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## アクセシビリティを意識したスピンボタンの作り方
### 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>
```

### ２. 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では、他のコンポーネントにも焦点を当てて、アクセシビリティについても解説しているので、ぜひ購読していてください。

https://qiita.com/advent-calendar/2023/degudegu2510

---

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

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

https://twitter.com/degudegu2510