三角関数を駆使してCSSだけでローディングスピナーを作ろう!

投稿日:

更新日:

今回作るもの

8つの丸が回転しているように見えるローディングスピナーをCSSだけで実装します。

CSSの値関数にsin()cos()といった数学関数があるのでこちらを使用します。

三角関数を使用してCSSで実装したローディングスピナー
三角関数を使用してCSSで実装したローディングスピナー

実装方法

HTMLを用意する

まず、ローディングスピナーの領域を確保するdivタグを作り、クラス名はloadingとしておきます。

HTML
<div class="loading"></div>

.loading-spinnerの子要素に、ドットを構成するspanタグを設置したいドットの数だけ追加します(今回は8つにします)。クラス名はdotとしておきます。
また、それぞれの.dotのstyle属性にインデックス番号を指定したカスタムプロパティを設定します。
カスタムプロパティは--iとし(indexの頭文字)、インデックス番号なので値は0〜7を順に指定します。

HTML
<div class="loading">
  <span class="dot" style="--i: 0"></span>
  <span class="dot" style="--i: 1"></span>
  <span class="dot" style="--i: 2"></span>
  <span class="dot" style="--i: 3"></span>
  <span class="dot" style="--i: 4"></span>
  <span class="dot" style="--i: 5"></span>
  <span class="dot" style="--i: 6"></span>
  <span class="dot" style="--i: 7"></span>
</div>

ちなみに...今回必要なHTMLをEmmetでまとめて記述したい場合は以下のように記述してからtabキーを押してください。

Emmet
.loading-spinner>span.dot[style="--i: $@0"]*8

カスタムプロパティを用意する

丸の数やサイズを変えたいときにコード内を行き来して編集箇所を探す手間を省くために、ローディングスピナーのルート要素となる.loading-spinnerにカスタムプロパティを定義します。

スピナーやドットのサイズやドットの数を変えたいときはここを編集することで調整できるようになります。

--spinnerRadiusは直径を2で割って半径を求めているので変更しないようにしてください。(直径とわけて指定するとほんの少しとはいえ手間が増えてしまいます。)

CSS
.loading-spinner {
  --spinnerDiameter: 40px; /* スピナーの直径 */
  --spinnerRadius: calc(var(--spinnerDiameter) / 2); /* スピナーの半径 */
  --dotCount: 8; /* ドットの数 */
  --dotDiameter: 8px; /* ドットの直径 */
}

スピナーの外枠を作成する

.loading-spinnerのCSSを書いていきます。

  • .dotposition: absolute;で配置していくので、外枠を基準とするためにposition: relative;を指定します。
  • スピナーのサイズは--spinnerDiameterで定義しているので、width: var(--spinnerDiameter);を指定します。
  • スピナーは正円にするため、aspect-ratio: 1;を指定します。

    ※万が一高さが合わない場合は、代わりにheight: var(--spinnerDiameter);を指定します。

CSS
.loading-spinner {
  /* (省略)カスタムプロパティの定義 */
  
  position: relative;
  width: var(--spinnerDiameter);
  aspect-ratio: 1; /* 高さが合わない場合はheightにする */
}

ドットを配置するための三角関数の活用

まずドットを円周上に等間隔で配置するために、高校数学で学習する三角関数についてのおさらいをしておきます。

x軸の正方向から角度θだけ傾いた直線が、半径rの円の円周と交わる位置(α, β)に点を配置することを考えます。

xとy軸に沿った円と、角度θでの三角関数の座標(r cos θ, r sin θ)を示す図
円における三角関数の関係を図示したもの

半径1の単位円のときは(x, y) = (cos θ, sin θ)となるので、半径rの場合は以下のように表せます。

(α,β)=(rcosθ,rsinθ)(\alpha, \beta) = (r \cos \theta, r \sin \theta)

ドットを配置する

.dotのCSSを書いていきます。

  • 幅を指定するためにdisplay: inline-block;を指定します。
  • width: var(--dotDeameter);aspect-ratio: 1;指定して、ドットを作ります。

    ※万が一高さが合わない場合は、代わりにheight: var(--dotDiameter);を指定します。

  • ドットの色はbackground-color: #ededed;でグレーを指定しておきます。
  • ドットは正円にしたいのでborder-radius: 50%;を指定します。
  • 円の中央にドットを配置するために、position, top, left, translateを指定します。
CSS
.dot {
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  display: inline-block;
  width: var(--dotDiameter);
  aspect-ratio: 1;
  background-color: #ededed;
  border-radius: 50%;
}

1つ目のドットの位置はx軸の正方向からθ傾いた直線と円周の交わる点(α, β)になります。
これを後ほど--angleとしてカスタムプロパティを定義します。

θ=360n\theta = \frac{360^\circ}{n}

次にθのときの(α, β)の値を求めます。こちらは三角関数のセクションに記載しているように以下の通りになります。
これを後ほど--x, --yとしてカスタムプロパティを定義します。

(α,β)=(rcosθ,rsinθ)(\alpha, \beta) = (r \cos \theta, r \sin \theta)

これらを踏まえて、.dotにCSSを追加します。
--angleは先ほどの360/nにインデックス番号(--i)をかけることで、8つのドットを円周上に均等に配置しています。

また、translateを記載のとおり差し替えます。--x--yが円周上の位置を示していますが、このままだと全体が右下にずれてしまいます。
そのため、元々指定していたようにx軸、y軸ともに-50%ずらします。

CSS
.dot {
  --angle: calc((360deg / var(--dotCount)) * var(--i));
  --x: calc(var(--spinnerRadius) * cos(var(--angle))); /* r cosθ */
  --y: calc(var(--spinnerRadius) * sin(var(--angle))); /* r sinθ */
  
  /* (省略)先ほど追加したCSS */
  
  translate: -50% -50%; /* 削除 */
  translate: calc(var(--x) - 50%) calc(var(--y) - 50%); /* 追加 */
}

これで円周上に8つのドットを均等に配置することができました。

以下の画像はわかりやすいように色を変えていますが、ドットの中心が円周上に乗るようにしているため、ローディングスピナーの領域(グレーの四角)からドットの半径の長さ分はみ出しています。
そこで.loading-spinnerにpaddingを追加します。また、box-size: border-box;だとpaddingの値もwidthに含むため、widthも本来のサイズに加えてドットの半径分の長さを左右に加算します。
すなわち、ドットの半径2つ分の長さを加算することになるので、ドットの直径を加算した記述にしています。

ローディングスピナーの中央にグレーの正方形があり、上下左右に回転半径の長さだけ赤い点がはみ出ている図
ルーディングスピナーのサイズに対して、上下左右が半径の長さだけ外にはみ出している
CSS
.loading-spinner {
  /* (省略)指定済みのCSS */
  
  width: calc(var(--spinnerDiameter) * var(--dotDiameter));
  padding: calc(var(--dotDiameter) / 2);
}

アニメーションを追加する

最後にドットにアニメーションを追加します。

まずサイズと色が変化するキーフレームアニメーションを指定します。
scaleは0%と100%のときに0にしたいので、.dotに追記します。

--delayは8個すべてで異なる値にならないと複数のドットが同時に拡大・縮小してしまうので、--durationには1周にかかる時間を指定し、それを8分割してそれぞれのドットに割り当てる指定にします。こうすることで1秒間に1/8ずつずらしてドットが動くようになります。

CSS
.dot {
  scale: 0;

  /* Animation Settings */
  --duration: 1s; /* 1周にかかる時間 */
  --delay: calc(var(--duration) * (var(--i) / var(--dotCount)));
  animation: loadingAnimation var(--duration) var(--delay) infinite ease-in-out;
}

@keyframes loadingAnimation {
  50% {
    scale: 1;
    background-color: #7e7e7e;
  }
}

これでローディングスピナーの完成です!

完成品

Codepenのプレビューが完成品になります。

※CSSの記述がここまでの解説と異なっていますが、リセットCSSやベースCSSを追加しているので、実際にはspinnerレイヤーの中のみ使用していただければ実装できます。

作成したコード

HTML
<div class="loading-spinner">
  <span class="dot" style="--i: 0"></span>
  <span class="dot" style="--i: 1"></span>
  <span class="dot" style="--i: 2"></span>
  <span class="dot" style="--i: 3"></span>
  <span class="dot" style="--i: 4"></span>
  <span class="dot" style="--i: 5"></span>
  <span class="dot" style="--i: 6"></span>
  <span class="dot" style="--i: 7"></span>
</div>
CSS
* {
  box-sizing: border-box;
}
.loading-spinner {
  --spinnerDiameter: 40px; /* スピナーの直径 */
  --spinnerRadius: calc(var(--spinnerDiameter) / 2); /* スピナーの半径 */
  --dotCount: 8; /* ドットの数 */
  --dotDiameter: 8px; /* ドットの直径 */
  
  position: relative;
  width: calc(var(--spinnerDiameter) + var(--dotDiameter));
  aspect-ratio: 1;
  padding: calc(var(--dotDiameter) / 2);
}
.dot {
  --angle: calc((360deg / var(--dotCount)) * var(--i));
  --x: calc(var(--spinnerRadius) * cos(var(--angle))); /* r cosθ */
  --y: calc(var(--spinnerRadius) * sin(var(--angle))); /* r sinθ */
  
  position: absolute;
  top: 50%;
  left: 50%;
  translate: calc(var(--x) - 50%) calc(var(--y) - 50%);
  scale: 0;
  display: inline-block;
  width: var(--dotDiameter);
  aspect-ratio: 1;
  background: #ededed;
  border-radius: 50%;
  
  /* Animation Settings */
  --duration: 1s;
  --delay: calc(var(--duration) * (var(--i) / var(--dotCount)));
  animation: loadingAnimation var(--duration) var(--delay) infinite ease-in-out;
}

@keyframes loadingAnimation {
  50% {
    scale: 1;
    background-color: #7e7e7e;
  }
}

参考・引用

この記事をシェアする