[初心者向け]アニメーション付きトグルボタンの作り方を解説

## はじめに
アプリケーション開発において、必ずと言って過言ではないボタン。
CSS当ててリッチなデザインを作ろうってパターンは非常に多いと思います。
hover, activeなどの状態で色が変わるというのはよくあると思いますが、アニメーションもつけるとなると一手間かかってどうやるの？となりがち。

スタイルの当て方はいろいろあるのでこれが正解というわけではないのですが、スタイルの当て方を自分の勉強がてら初学者向けにまとめてみようと思います。

## 成果物
以下の様なスライド式ボタンを作成したいと思います。
初期状態は`hoge`が選択されていて、`fuga`をクリックすると背景色がアニメーションで移動して、ボタンの下のテキストが選択されたボタンに応じて変化します。
テキストが変化するのはレンダリングを切り替える様な挙動をイメージしています。

<p class="codepen" data-height="300" data-default-tab="css,result" data-slug-hash="qEBEppK" data-pen-title="base-button-sample7" data-user="Yoshi2885" 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/Yoshi2885/pen/qEBEppK">
  base-button-sample7</a> by Yoshi2885 (<a href="https://codepen.io/Yoshi2885">@Yoshi2885</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

地味に、クリックできる領域だけカーソルがポインターになる様なスタイルも当てています。


## ベースを作成する
まずはスタイルが当たっていない状態のベースを作成したいと思います。

CodePenだと以下の通りです
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="mydyRWp" data-pen-title="base-button-sample1" data-user="Yoshi2885" 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/Yoshi2885/pen/mydyRWp">
  base-button-sample1</a> by Yoshi2885 (<a href="https://codepen.io/Yoshi2885">@Yoshi2885</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

Reactの中身は、単純にボタンを２個配置したものと、Stateに応じて表示を切り替える部分を作成しました。
まだ、Styleは何も当てていない状態です。

CodePen見ればコード載せているので分かりますが、一応載せておきます。
```tsx
const ChildComponent = () => {
  const [isHogeChecked, setIsHogeChecked] = useState<boolean>(true);
  return (
    <>
      <div>
        <button>hoge</button>
        <button>fuga</button>
      </div>
      {isHogeChecked ? (
        <div>hoge is selected</div>
      ) : (
        <div>fuga is selected</div>
      )}
    </>
  );
};
```

## ボタンを押した時の挙動を追加する
次に、ボタンを押した時にStateを切り替えるための`onClick`メソッドを作成します。

```diff_tsx
const ChildComponent = () => {
    const [isHogeChecked, setIsHogeChecked] = useState<boolean>(true);
  return (
    <>
      <div>
-       <button>hoge</button>
+       <button onClick={() => setIsHogeChecked(true)}>hoge</button>

-       <button>fuga</button>
+       <button onClick={() => setIsHogeChecked(false)}>hoge</button>
      </div>
      {isHogeChecked ? (
        <div>hoge is selected</div>
      ) : (
        <div>fuga is selected</div>
      )}
    </>
  );
};
```
これで、ボタンをクリックするとStateが切り替わり、
```tsx
      {isHogeChecked ? (
        <div>hoge is selected</div>
      ) : (
        <div>fuga is selected</div>
      )}

```
この部分の表示が切り替わる様になります。
これで、Styleを当てる以外の要素は完成です。

## ボタンの背景を作成する
まずは、２個のボタンをひとまとめにする背景を作成します。
二つのbuttonタグを囲っているdivタグにclassNameを付与します。
```diff_tsx
const ChildComponent = () => {
  const [isHogeChecked, setIsHogeChecked] = useState<boolean>(true);
  return (
    <>
-     <div>
+     <div className="toggleArea">
        <button onClick={() => setIsHogeChecked(true)}>hoge</button>
        <button onClick={() => setIsHogeChecked(false)}>hoge</button>
      </div>
      {isHogeChecked ? (
        <div>hoge is selected</div>
      ) : (
        <div>fuga is selected</div>
      )}
    </>
  );
};
```

サクッと背景をつけると以下の様なscssを当てます。
(*.module.scssを日頃使うのですが今回はCodePen仕様でStyleを当てます)

```scss
.toggleArea {
  background-color: #eaedf1;
  display: flex;
  flex-wrap: wrap;
  width: 272px;
  height: 24px;
  padding: 4px;
  border-radius: 4px;
  margin-top: 16px;
  margin-bottom: 16px;
}
```

ここまでやるとグレー背景の中にボタンが並んだ状態になります。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="bNGNYVK" data-pen-title="base-button-sample1" data-user="Yoshi2885" 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/Yoshi2885/pen/bNGNYVK">
  base-button-sample1</a> by Yoshi2885 (<a href="https://codepen.io/Yoshi2885">@Yoshi2885</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

## ボタンのスタイルを調整する
まずは、デフォルトのボタンのスタイルを消しながら、
グレー背景の中でいい感じの位置にテキストが表示される様に修正をしていきます。

`hoge` `fuga` ボタンのどちらも同じスタイルを当てていこうと思うので同じクラス名を与えます。

```diff_tsx
const ChildComponent = () => {
  const [isHogeChecked, setIsHogeChecked] = useState<boolean>(true);
  return (
    <>
      <div className="toggleArea">
-       <button onClick={() => setIsHogeChecked(true)}>hoge</button>
+       <button className="toggleButton" onClick={() => setIsHogeChecked(true)}>hoge</button>

-       <button onClick={() => setIsHogeChecked(false)}>hoge</button>
+       <button className="toggleButton" onClick={() => setIsHogeChecked(false)}>hoge</button>
      </div>
      {isHogeChecked ? (
        <div>hoge is selected</div>
      ) : (
        <div>fuga is selected</div>
      )}
    </>
  );
};
```

`.toggleButton` にスタイルを当てます。

やりたいことは、
1. 全体を埋めるように横幅を調整
1. defaultの色やボーダーを削除
1. テキストのサイズを調整
1. カーソルをポインターに変更

です。

```diff_scss
.toggleArea {
  background-color: #eaedf1;
  display: flex;
  position: relative;
  align-items: center;
  align-content: center;
  flex-wrap: wrap;
  width: 272px;
  height: 24px;
  padding: 4px;
  border-radius: 4px;
  margin-top: 16px;
  margin-bottom: 16px;
}

+ .toggleButton {
+   flex: 1;
+   border: none;
+   font-size: 14px;
+   cursor: pointer;
+ }
```

ここまででこの様な形になります。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="bNGNYej" data-pen-title="base-button-sample1" data-user="Yoshi2885" 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/Yoshi2885/pen/bNGNYej">
  base-button-sample1</a> by Yoshi2885 (<a href="https://codepen.io/Yoshi2885">@Yoshi2885</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

## スライドする白背景を作成する
現状ではグレー背景しかないのでどちらが選択されているか視認できません。
そのため、グレー背景の擬似要素として白背景のスライダーを作成します。

```diff_scss
.toggleArea {
  background-color: #eaedf1;
  display: flex;
  width: 272px;
  height: 24px;
  padding: 4px;
  border-radius: 4px;
  margin-top: 16px;
  margin-bottom: 16px;

+ &::before {
+   content: "";
+   height: 24px;
+   width: 136px;
+   background-color: white;
+   border-radius: 4px;
+ }
}

.toggleButton {
  flex: 1;
  border: none;
  font-size: 14px;
  cursor: pointer;
}
```

擬似要素は、`content`に空文字を入れないと表示されないので忘れない様にしてください。
`height`, `width`で要素の大きさを指定し、`background-color`で色を調整します。
`border-radius`を背景のグレーと同じように4pxでつけて形を調整します。

このままでは、擬似要素はボタンの上に期待通りには表示されません。
背景のグレーとの位置調整が必要になるので`position`を親要素と擬似要素にそれぞれつけます。

```diff_scss
.toggleArea {
  background-color: #eaedf1;
  display: flex;
+ position: relative;
  width: 272px;
  height: 24px;
  padding: 4px;
  border-radius: 4px;
  margin-top: 16px;
  margin-bottom: 16px;

  &::before {
    content: "";
+   position: absolute;
+   top: 4px;
+   left: 4px;
    height: 24px;
    width: 136px;
    background-color: white;
    border-radius: 4px;
  }
}

.toggleButton {
  flex: 1;
  border: none;
  font-size: 14px;
  cursor: pointer;
}
```

:::note info
`top`, `left` の位置調整は、CodePen上では不要でしたが、ローカルでやってみるとズレたのでつけています。
:::


ここまでの状態は以下の通りです。

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="YPzPEQx" data-pen-title="base-button-sample2" data-user="Yoshi2885" 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/Yoshi2885/pen/YPzPEQx">
  base-button-sample2</a> by Yoshi2885 (<a href="https://codepen.io/Yoshi2885">@Yoshi2885</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

見てみると、擬似要素がボタンのテキストの上に来てしまい、`hoge`の文字が隠れてしまいました。
なので、`z-index`を調整して、`hoge`を全面に出そうと思います。

```diff_scss
.toggleArea {
  background-color: #eaedf1;
  display: flex;
  position: relative;
  width: 272px;
  height: 24px;
  padding: 4px;
  border-radius: 4px;
  margin-top: 16px;
  margin-bottom: 16px;

  &::before {
    content: "";
    position: absolute;
    top: 4px;
    left: 4px;
    height: 24px;
    width: 136px;
    background-color: white;
    border-radius: 4px;
  }
}

.toggleButton {
  flex: 1;
  border: none;
  font-size: 14px;
  cursor: pointer;
+ z-index: 1;
}
```

そうすると、ボタンの色に隠れて擬似要素の背景が見えなくなってしまったので、
ボタンの背景を透過しようと思います。

```diff_scss
.toggleArea {
  background-color: #eaedf1;
  display: flex;
  position: relative;
  width: 272px;
  height: 24px;
  padding: 4px;
  border-radius: 4px;
  margin-top: 16px;
  margin-bottom: 16px;

  &::before {
    content: "";
    position: absolute;
    top: 4px;
    left: 4px;
    height: 24px;
    width: 136px;
    background-color: white;
    border-radius: 4px;
  }
}

.toggleButton {
  flex: 1;
  border: none;
  font-size: 14px;
+ background: transparent;
  cursor: pointer;
  z-index: 1;
}
```

これで背景はいい感じに見える様になりました。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="emYmeKp" data-pen-title="base-button-sample3" data-user="Yoshi2885" 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/Yoshi2885/pen/emYmeKp">
  base-button-sample3</a> by Yoshi2885 (<a href="https://codepen.io/Yoshi2885">@Yoshi2885</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

## 白背景を動かす
ボタンのクリックによって背景の表示位置を調整したいと思います。
今回は、`isHogeChecked`のステートを使って切り替えを行いたいと思います。
まず、親要素の`div`タグに`data-checked`でStateの状態を持たせます。

```diff_tsx
const ChildComponent = () => {
  const [isHogeChecked, setIsHogeChecked] = useState<boolean>(true);
  return (
    <>
-       <div className="toggleArea">
+       <div className="toggleArea" data-checked={isHogeChecked}>
        <button
          className="toggleButton"
          onClick={() => setIsHogeChecked(true)}
        >
          hoge
        </button>
        <button
          className="toggleButton"
          onClick={() => setIsHogeChecked(false)}
        >
          fuga
        </button>
      </div>
      {isHogeChecked ? (
        <div>hoge is selected</div>
      ) : (
        <div>fuga is selected</div>
      )}
    </>
  );
};
```

この`data-checked`を使って擬似要素の表示位置を`transform`, `translateX`を使って変更します。

```diff_scss
.toggleArea {
  background-color: #eaedf1;
  display: flex;
  position: relative;
  width: 272px;
  height: 24px;
  padding: 4px;
  border-radius: 4px;
  margin-top: 16px;
  margin-bottom: 16px;

  &::before {
    content: "";
    position: absolute;
    top: 4px;
    left: 4px;
    height: 24px;
    width: 136px;
    background-color: white;
    border-radius: 4px;
  }

+  &[data-checked='false']::before {
+    transform: translateX(136px);
+  }
}

.toggleButton {
  flex: 1;
  border: none;
  font-size: 14px;
  background: transparent;
  cursor: pointer;
  z-index: 1;
}
```

この状態まで来るとこの様な表示になります。
ほぼ完成している感じです👍
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="ogNgoRp" data-pen-title="base-button-sample5" data-user="Yoshi2885" 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/Yoshi2885/pen/ogNgoRp">
  base-button-sample5</a> by Yoshi2885 (<a href="https://codepen.io/Yoshi2885">@Yoshi2885</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

## 動作にアニメーションをつける
白背景のスライダーが移動するようになったので、この動作にアニメーションをつけます。

```diff_scss
.toggleArea {
  background-color: #eaedf1;
  display: flex;
  position: relative;
  width: 272px;
  height: 24px;
  padding: 4px;
  border-radius: 4px;
  margin-top: 16px;
  margin-bottom: 16px;

  &::before {
    content: "";
    position: absolute;
    top: 4px;
    left: 4px;
    height: 24px;
    width: 136px;
    background-color: white;
    border-radius: 4px;
+   transition: transform 0.3s ease;
  }

  &[data-checked='false']::before {
    transform: translateX(136px);
  }
}

.toggleButton {
  flex: 1;
  border: none;
  font-size: 14px;
  background: transparent;
  cursor: pointer;
  z-index: 1;
}
```

この1行だけ追加すればOKです。
アニメーションの動きの部分は、`ease`にしていますが、
`ease-in-out`などもよく使われているのでいろいろ試してみてください。

::: note info
動きの部分は`cubic-bezier`を使うと自作の動きを作ることも出来ます
:::


<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="PwowENg" data-pen-title="base-button-sample6" data-user="Yoshi2885" 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/Yoshi2885/pen/PwowENg">
  base-button-sample6</a> by Yoshi2885 (<a href="https://codepen.io/Yoshi2885">@Yoshi2885</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

## スタイルの微調整
動きとしては9割完了しているのですが、もう少しだけスタイルにこだわりたいと思います。

具体的には、
- ポインターの調整
- フォントカラーの調整

を行いたいと思います。
スライダーが選択されているところはボタンをクリックしても反応しないので、カーソルをポインターにしない様にするのと、非選択になっているボタンのテキストカラーをグレーよりに変更して選択されている方がより目立つ様にしたいと思います。

ボタンのどちらがアクティブになっているのかを`isHogeChecked`のStateを使ってクラスを付与し、そこにスタイルを当てていく戦略を取りたいと思います。

```diff_tsx
const ChildComponent = () => {
  const [isHogeChecked, setIsHogeChecked] = useState<boolean>(true);
  return (
    <>
      <div className="toggleArea" data-checked={isHogeChecked}>
        <button
-         className="toggleButton"
+         className={`toggleButton ${isHogeChecked ? "active" : ""}`}
          onClick={() => setIsHogeChecked(true)}
        >
          hoge
        </button>
        <button
-         className="toggleButton"
+         className={`toggleButton ${!isHogeChecked ? "active" : ""}`}
          onClick={() => setIsHogeChecked(false)}
        >
          fuga
        </button>
      </div>
      {isHogeChecked ? (
        <div>hoge is selected</div>
      ) : (
        <div>fuga is selected</div>
      )}
    </>
  );
};
```

`active`のクラスが付与されている状態が、白背景のスライダーがある状態なので、カーソルと文字色の調整を行います。

```diff_scss
.toggleArea {
  background-color: #eaedf1;
  display: flex;
  position: relative;
  width: 272px;
  height: 24px;
  padding: 4px;
  border-radius: 4px;
  margin-top: 16px;
  margin-bottom: 16px;

  &::before {
    content: "";
    position: absolute;
    top: 4px;
    left: 4px;
    height: 24px;
    width: 136px;
    background-color: white;
    border-radius: 4px;
    transition: transform 0.3s ease;
  }

  &[data-checked="false"]::before {
    transform: translateX(136px);
  }
}

.toggleButton {
  flex: 1;
  border: none;
  font-size: 14px;
+ color: #8293ab;
  cursor: pointer;
  background: transparent;
  z-index: 1;

+  &.active {
+    color: #2a3c56;
+    cursor: default;
+  }
}
```

これが完成系になります。

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="qEBEppK" data-pen-title="base-button-sample7" data-user="Yoshi2885" 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/Yoshi2885/pen/qEBEppK">
  base-button-sample7</a> by Yoshi2885 (<a href="https://codepen.io/Yoshi2885">@Yoshi2885</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

::: note warn
CodePen上では綺麗にスタイルが当たっていますが、
環境に応じて追加でスタイルの調整が必要な場合もあります。
その場合は、各自で調整をお願いします。
:::

## 最後に
比較的シンプルなトグルのスイッチをアニメーション付きで作成してみました。
トグルのOn/Offに応じて背景を変化させたり、その他のStateを活用しながらより複雑なアニメーション(例えば、クリック→送信中→送信完了を連続的に遷移させるなど)を作れる様になると、CSSともっとお友達になれるかも知れません。

ネット上に、コピペOK！というスタイルがたくさん転がっていますが、自分でどうやって作ればいいんだろう？というのを実際にやってみるとかなりいい学びになるので是非色々試しに作ってみてください！

今回の記事で、手順をまとめてみましたが、これが絶対の正解ではないので、モダンな書き方とか、もっとこうやったらシンプルに記述できるとか可読性が高くなるとか…先輩方の知見をコメント欄でいただけたらありがたいです。