三角関数を駆使してCSSだけでローディングスピナーを作ろう!
投稿日:
更新日:
今回作るもの
8つの丸が回転しているように見えるローディングスピナーをCSSだけで実装します。
CSSの値関数にsin()
やcos()
といった数学関数があるのでこちらを使用します。
実装方法
HTMLを用意する
まず、ローディングスピナーの領域を確保するdiv
タグを作り、クラス名はloading
としておきます。
<div class="loading"></div>
.loading-spinner
の子要素に、ドットを構成するspan
タグを設置したいドットの数だけ追加します(今回は8つにします)。クラス名はdot
としておきます。
また、それぞれの.dot
のstyle属性にインデックス番号を指定したカスタムプロパティを設定します。
カスタムプロパティは--i
とし(indexの頭文字)、インデックス番号なので値は0〜7
を順に指定します。
<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キーを押してください。
.loading-spinner>span.dot[style="--i: $@0"]*8
カスタムプロパティを用意する
丸の数やサイズを変えたいときにコード内を行き来して編集箇所を探す手間を省くために、ローディングスピナーのルート要素となる.loading-spinner
にカスタムプロパティを定義します。
スピナーやドットのサイズやドットの数を変えたいときはここを編集することで調整できるようになります。
--spinnerRadius
は直径を2で割って半径を求めているので変更しないようにしてください。(直径とわけて指定するとほんの少しとはいえ手間が増えてしまいます。)
.loading-spinner {
--spinnerDiameter: 40px; /* スピナーの直径 */
--spinnerRadius: calc(var(--spinnerDiameter) / 2); /* スピナーの半径 */
--dotCount: 8; /* ドットの数 */
--dotDiameter: 8px; /* ドットの直径 */
}
スピナーの外枠を作成する
.loading-spinner
のCSSを書いていきます。
.dot
をposition: absolute;
で配置していくので、外枠を基準とするためにposition: relative;
を指定します。- スピナーのサイズは
--spinnerDiameter
で定義しているので、width: var(--spinnerDiameter);
を指定します。 - スピナーは正円にするため、
aspect-ratio: 1;
を指定します。※万が一高さが合わない場合は、代わりに
height: var(--spinnerDiameter);
を指定します。
.loading-spinner {
/* (省略)カスタムプロパティの定義 */
position: relative;
width: var(--spinnerDiameter);
aspect-ratio: 1; /* 高さが合わない場合はheightにする */
}
ドットを配置するための三角関数の活用
まずドットを円周上に等間隔で配置するために、高校数学で学習する三角関数についてのおさらいをしておきます。
x軸の正方向から角度θ
だけ傾いた直線が、半径r
の円の円周と交わる位置(α, β)
に点を配置することを考えます。
半径1の単位円のときは(x, y) = (cos θ, sin θ)
となるので、半径rの場合は以下のように表せます。
ドットを配置する
.dot
のCSSを書いていきます。
- 幅を指定するために
display: inline-block;
を指定します。 width: var(--dotDeameter);
とaspect-ratio: 1;
指定して、ドットを作ります。※万が一高さが合わない場合は、代わりに
height: var(--dotDiameter);
を指定します。- ドットの色は
background-color: #ededed;
でグレーを指定しておきます。 - ドットは正円にしたいので
border-radius: 50%;
を指定します。 - 円の中央にドットを配置するために、
position
,top
,left
,translate
を指定します。
.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
としてカスタムプロパティを定義します。
次にθ
のときの(α, β)
の値を求めます。こちらは三角関数のセクションに記載しているように以下の通りになります。
これを後ほど--x
, --y
としてカスタムプロパティを定義します。
これらを踏まえて、.dot
にCSSを追加します。--angle
は先ほどの360/nにインデックス番号(--i
)をかけることで、8つのドットを円周上に均等に配置しています。
また、translate
を記載のとおり差し替えます。--x
と--y
が円周上の位置を示していますが、このままだと全体が右下にずれてしまいます。
そのため、元々指定していたようにx軸、y軸ともに-50%ずらします。
.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つ分の長さを加算することになるので、ドットの直径を加算した記述にしています。
.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ずつずらしてドットが動くようになります。
.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レイヤーの中のみ使用していただければ実装できます。
作成したコード
<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>
* {
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;
}
}