おしゃれなアニメーション送信ボタンの作り方を解説

## はじめに
タイトルの通り、状態に合わせて遷移するようなアニメーションを付与したボタンの作り方を解説したいと思います。

::: note warn
2025年 2/21更新
スマホ版で見ると、ボタンの`submit`のテキストの色が、buttonタグデフォルトの青色表示になっていることがわかったので、`color: black`を`submitButton`クラスに追加しました。
:::

## 完成予想図
出来上がりのボタンは以下の通りです。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="ogNjwqx" data-pen-title="sending-spinner-button6" 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/ogNjwqx">
  sending-spinner-button6</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>
`submit`と表示されているボタンをクリックすると、
送信中を示すクルクルが表示され、送信が完了したら✅マークが表示される様なものをイメージしています。

## 作り方
### ボタンを用意する
最初は、スタイルが全くない状態のボタンを用意します。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="MYWamKm" data-pen-title="sending-spinner-button1" 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/MYWamKm">
  sending-spinner-button1</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の部分を解説します。

コンポーネント部分の全文は以下の通りです。
```tsx
const ChildComponent = () => {
  const [status, setStatus] = useState<"idle" | "sending" | "success">("idle");
  const clickAction = () => {
    if (status !== "idle") return;

    setStatus("sending");
    setTimeout(() => {
      setStatus("success");
      setTimeout(() => {
        setStatus("idle");
      }, 1500);
    }, 2500);
  };

  return (
    <>
      <div className="submitButtonContainer">
        <button className={`submitButton ${status}`} onClick={clickAction}>
          {status === "idle" ? "submit" : <></>}
        </button>
      </div>
    </>
  );
};
```

まずは、Buttonの部分から
```tsx
  return (
    <>
      <div className="submitButtonContainer">
        <button className={`submitButton ${status}`} onClick={clickAction}>
          {status === "idle" ? "submit" : <></>}
        </button>
      </div>
    </>
  );
};
```

`button`タグには、`className`を`submitButton` と `useState`で作っている`status`を入れています。
この`status`には、`idle`, `sending`, `success`の３種類の状態がそれぞれ入る様にしています。

したがって、共通の`submitButton`クラスにデフォルトのボタンのスタイルを入れて、
`submitButton` + `idle` のクラスが付与されている時はボタンが押される前の状態のスタイルを
`submitButton` + `sending` のクラスが付与されている時は送信中のスタイルを
`submitButton` + `success` のクラスが付与されている時は送信完了時のスタイルをそれぞれ当てる様な戦略です。

ボタンの中身のテキストは、
```tsx
  {status === "idle" ? "submit" : <></>}
```
としていますが、
`idle`の状態の時のみ`submit`の文字が必要なため三項演算子を用いて表現しています。
(もっと賢いやり方ある様な気もする…。)

`onClick`のメソッドは、
```tsx
  const clickAction = () => {
    if (status !== "idle") return;

    setStatus("sending");
    setTimeout(() => {
      setStatus("success");
      setTimeout(() => {
        setStatus("idle");
      }, 1500);
    }, 2500);
  };
```
としてあり、擬似的に2.5s `sending`状態を維持し、次に 1.5s `success`状態を維持し、最後に`idle`状態に戻るような挙動を示すような形にしています。

また、1行目の`if(status !== "idle") return`で、ボタンを連打しても1回しか反応しないようにしてあります。

ここまで何度も出てきている`status`の部分は、
```tsx
  const [status, setStatus] = useState<"idle" | "sending" | "success">("idle");
```
としていて、`idle`, `sending`, `success`の文字列のみ入る`useState`になっています。

### 共通のStyleを当てる
まずは、初期状態を示す様なスタイルを当てていこうと思います。

一番はじめに`button`タグを囲っている`div`タグの`submitButtonContainer`クラスにスタイルを当てて、いい感じの位置にボタンを表示させる様にします。

```diff_scss
+ .submitButtonContainer {
+   display: flex;
+   justify-content: center;
+   align-items: center;
+   height: 80px;
+ }
```

次に、`button`タグの`submitButton`クラスにスタイルを当てます。

```diff_scss
.submitButtonContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80px;
  
+ .submitButton {
+   width: 120px;
+   height: 36px;
+   border-radius: 18px;
+   font-weight: 600;
+   font-size: 14px;
+   color: black;
+ }
}
```
ここまで作ると、以下の様になります。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="dPyYWZr" data-pen-title="sending-spinner-button1" 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/dPyYWZr">
  sending-spinner-button1</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>

ボーダーやボタンの背景などの色が違いますが、概形は完成しました。

### idle状態のStyleを当てる
共通状態のスタイルを当てた段階で、基本的な部分は完成しているので、サクッと`idle`状態のスタイルを当てていきます。

```diff_scss
.submitButtonContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80px;

  .submitButton {
    width: 120px;
    height: 36px;
    border-radius: 18px;
    font-weight: 600;
    font-size: 14px;
    color: black;

+   &.idle {
+     background-color: white;
+     border: 2px solid coral;
+     cursor: pointer;
+   }
  }
}
```

`&.idle`と表記することで、`submitButton`クラス と`idle`クラスの両方が当たっている時のスタイルを記述することができます。

ここでは
- `background-color`で背景色を白に
- `border`で枠線の太さと色を
- `cursor`でマウスカーソルの状態を

それぞれ指定しました。

さらに、完成図のボタンを見ると、ホバーさせた時に背景色と文字色が変わっているので、scssに追加します。

```diff_scss
.submitButtonContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80px;

  .submitButton {
    width: 120px;
    height: 36px;
    border-radius: 18px;
    font-weight: 600;
    font-size: 14px;
    color: black;

    &.idle {
      background-color: white;
      border: 2px solid coral;
      cursor: pointer;

+     &:hover {
+       background-color: coral;
+       color: white;
+     }
    }
  }
}

```

`&:hover`を定義するネストの場所に注意です。
`submitBtton`, `idle`クラスの２種類が共に当たっている時に`hover`させると色が変わる様にしたいので、
`&.idle`の中に記述する必要があります。

ここまで作ると以下の様になります。

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="EaxVmRe" data-pen-title="sending-spinner-button2" 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/EaxVmRe">
  sending-spinner-button2</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>

ホバーさせると色が変わる様になりましたね！💡

### sending状態のStyleを作る
いよいよここからが本題です。
ボタンをクリックしたタイミングで`sending`のクラスが当たる様になるので今度は、
`submitButton` + `sending`のクラスが当たった時にクルクルが出るようにします。

```diff_scss
.submitButtonContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80px;

  .submitButton {
    width: 120px;
    height: 36px;
    border-radius: 18px;
    font-weight: 600;
    font-size: 14px;
    color: black;

    &.idle {
      background-color: white;
      border: 2px solid coral;
      cursor: pointer;

      &:hover {
        background-color: coral;
        color: white;
      }
    }

+   &.sending {
+     width: 36px;
+     background-color: white;
+     cursor: default;
+     border: 3px solid lightgray;
+     border-top-color: coral;
+   }
  }
}

```

追加したのは、
- `width`で幅を高さと同じ36pxにして円を作る
- `background-color`で円の中央の色を白くする
- `corsor`を`default`にすることで矢印ポインターに変更(クリックできなさそうに見える)
- `border`でクルクルの円の太さ指定し色を明るいグレーに調整
- `border-top-color`で上の4分の1の範囲だけ色を変更

しています。
ここまで作成すると以下の通りです。
ボタンをクリックすると円に変わるのがわかると思います。

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="OPJymBX" data-pen-title="sending-spinner-button3" 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/OPJymBX">
  sending-spinner-button3</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
.submitButtonContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80px;

  .submitButton {
    width: 120px;
    height: 36px;
    border-radius: 18px;
    font-weight: 600;
    font-size: 14px;
    color: black;

    &.idle {
      background-color: white;
      border: 2px solid coral;
      cursor: pointer;

      &:hover {
        background-color: coral;
        color: white;
      }
    }

    &.sending {
      width: 36px;
      background-color: white;
      cursor: default;
      border: 3px solid lightgray;
      border-top-color: coral;
+     animation: spin 2s 0.25s linear infinite;
    }
  }
}

+ @keyframes spin {
+   from {
+     transform: rotate(0deg);
+   }
+   to {
+     transform: rotate(360deg);
+   }
}
```
と記述します。
`&.sending`の中で、`animation`を指定します。
今回は、無限に回転させたいので、
`@keyframes`で`spin`を定義し、0 ~ 360 deg 回転する様に設定します。

```scss
  animation: spin 2s 0.25s linear infinite;
```

の部分は、
- 動作は`spin`のkeyframesを使用する
- `2s`で一回転
- アニメーションの開始は`0.25s`遅延させる
- `linear`で一定速度に回転させる(加減速させない)
- `infinte`で無限に繰り返し処理を行う

という処理を記述しています。

ここまで実装できたらこの様になります。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="bNGVWJq" data-pen-title="sending-spinner-button4" 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/bNGVWJq">
  sending-spinner-button4</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>

だいぶそれっぽくなりました💡

### success状態のStyleを作成する
３つ目の状態である送信完了を示す`success`状態のスタイルを当てていきます。
基本は`idle`状態のボタンと同じ様なスタイルで、テキストがチェックマークであるところがちょっと違う…という感じです。

まずは、ボタンの背景色を調整します。
```diff_scss
.submitButtonContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80px;

  .submitButton {
    width: 120px;
    height: 36px;
    border-radius: 18px;
    font-weight: 600;
    font-size: 14px;
    color: black;

    &.idle {
      background-color: white;
      border: 2px solid coral;
      cursor: pointer;

      &:hover {
        background-color: coral;
        color: white;
      }
    }

    &.sending {
      width: 36px;
      background-color: white;
      cursor: default;
      border: 3px solid lightgray;
      border-top-color: coral;
      animation: spin 2s 0.25s linear infinite;
    }

+   &.success {
+     background-color: coral;
+     border: 2px solid coral;
+   }
  }
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
```

たった4行の追加しかありませんが、今の状態でボタンをクリックするとこの様な形になります。

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="MYWaoKB" data-pen-title="sending-spinner-button4" 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/MYWaoKB">
  sending-spinner-button4</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
今回、チェックマークは`FontAwesome`を使用しています。

そのため、CodePenではSettingからHTMLの部分を開き、
![CodePen_Setting](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3750623/9c4b45d9-8082-4b33-9707-0dd5de13b9cb.png)
`Stuff for <head>`の部分に、
```plaintext
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
```
を入れてください。
Reactの場合は、`npm`や`yarn`などで`fontAwesome`が使えるようにinstallをしてください。
:::

擬似要素の部分のスタイルは以下の通りです。
```diff_scss
.submitButtonContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80px;

  .submitButton {
    width: 120px;
    height: 36px;
    border-radius: 18px;
    font-weight: 600;
    font-size: 14px;
    color: black;

    &.idle {
      background-color: white;
      border: 2px solid coral;
      cursor: pointer;

      &:hover {
        background-color: coral;
        color: white;
      }
    }

    &.sending {
      width: 36px;
      background-color: white;
      cursor: default;
      border: 3px solid lightgray;
      border-top-color: coral;
      animation: spin 2s 0.25s linear infinite;
    }

    &.success {
      background-color: coral;
      border: 2px solid coral;

+     &:after {
+       font-family: "Font Awesome 5 Free", sans-serif;
+       content: "\f00c";
+       font-weight: 900;
+       color: white;
+       font-size: 20px;
+     }
    }
  }
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
```

`font-family`で`FontAwesome`を使用することを定義しています。
`content`で`\f00c`と書いていますがチェックマークを表しています。
`font-weight`は900にしていますが好みの問題です←
`color`で文字色を白色にし、
`font-size`は意外とチェックマークが小さかったのでバランスを取るために大きくしています。

まだ、パラパラ漫画のようなボタンの動作になっていますが、ほぼ完成した状態がこちらです。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="YPzyQGw" data-pen-title="sending-spinner-button5" 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/YPzyQGw">
  sending-spinner-button5</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
.submitButtonContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80px;

  .submitButton {
    width: 120px;
    height: 36px;
    border-radius: 18px;
    font-weight: 600;
    font-size: 14px;
    color: black;
+   transition: all 0.3s ease-in-out;

    &.idle {
      background-color: white;
      border: 2px solid coral;
      cursor: pointer;

      &:hover {
        background-color: coral;
        color: white;
      }
    }

    &.sending {
      width: 36px;
      background-color: white;
      cursor: default;
      border: 3px solid lightgray;
      border-top-color: coral;
      animation: spin 2s 0.25s linear infinite;
    }

    &.success {
      background-color: coral;
      border: 2px solid coral;

      &:after {
        font-family: "Font Awesome 5 Free", sans-serif;
        content: "\f00c";
        font-weight: 900;
        color: white;
        font-size: 20px;
      }
    }
  }
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
```

最後1行追加するだけですが、パラパラ漫画の様だったボタンがぬるぬる動く様になります。
これで完成です。

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="ogNjwqx" data-pen-title="sending-spinner-button6" 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/ogNjwqx">
  sending-spinner-button6</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>

## 最後に
ネットに転がっていたおしゃれなボタンを自分で実装してみたのですが、完成状態のコードはあれど作り方ってなかなか載ってなくてどんなステップで作成すればいいか？が知りたいのに…となったのが正直なところでした。
せっかく自分で作ったので、そういった解説があってもいいかなと思い今回記事にしてみました。

完全に余談になりますが、日頃、scssは*.module.scssを使用しているので、
`className`の部分は、
```tsx
  <div className={styles.submitButtonContainer}>
    <button
      className={`${styles.submitButton} ${styles[status]}`}
      onClick={clickAction}
    >
      {status === 'idle' ? 'submit' : <></>}
    </button>
  </div>
```
この様に記述すると、CodePenのクラスと同じように設定することができます。
特に注意しないといけないのが、
```tsx
  className={`${styles.submitButton} ${styles[status]}`}
```
の部分で、
```tsx
  className={`${styles.submitButton} ${styles.status}`}
```
この様に書いてしまい、
あれ？Style全然当たらねぇ…。とハマりやすいので要注意です。
`styles.status`と書いてしまうと、`status`クラスが付与されてしまいます💦(直近3連敗中)

複数クラスを当てながら上手に状態遷移させることでおしゃれなアニメーションのボタンが作れる様になりますのでぜひ参考にしながら実装に挑戦してみてください。