本文へスキップ
メニュー
テーマ

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

目次

今回作るもの

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

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

CSS 値関数 - CSS | MDN⁠(新しいタブで開く)

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

実装方法

HTMLを用意する

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

HTMLHTML · 1行
<div class="loading-spinner"></div>

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

HTMLHTML · 10行
<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>

ちなみに、今回必要なHTMLをEmmet⁠(新しいタブで開く)でまとめて記述したい場合は以下のように記述してからTabキーを押してください。

テキストテキスト · 1行
.loading-spinner>span.dot[style="--i: $@0"]*8

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

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

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

--spinnerRadiusは直径を2で割って半径を求めているので変更しないようにしてください(直径と分けて指定すると、サイズを変えるたびに両方を直す手間が増えてしまいます)。

CSSCSS · 6行
.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);を指定します。

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

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

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

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

半径rの円周上の点を、角度θと半径rで表す

半径rの円周上の点を、角度θと半径rで表す

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

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

ドットを配置する

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

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

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

  • ドットの色はbackground-color: #ededed;でグレーを指定しておきます。
  • ドットは正円にしたいのでborder-radius: 50%;を指定します。
  • 円の中央にドットを配置するために、position, top, left, translateを指定します。
CSSCSS · 11行
.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%;}

ドットは円周上に等間隔に並べるので、隣り合うドットの間の角度θは、360°をドットの数nで割った値になります。
これを後ほど--angleとしてカスタムプロパティを定義します。

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

(nはドットの数)

次に、x軸の正方向からθ傾いた位置にあるドットの座標(α, β)を求めます。こちらは三角関数のセクションに記載しているように以下のとおりになります。
これを後ほど--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%ずらします。

CSSCSS · 10行
.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-sizing: border-box;だとpaddingの値もwidthに含むため、widthも本来のサイズに加えてドットの半径分の長さを左右に加算します。
すなわち、ドットの半径2つ分の長さを加算することになるので、ドットの直径を加算した記述にしています。

ローディングスピナーのサイズに対して、上下左右が半径の長さだけ外にはみ出している

ローディングスピナーのサイズに対して、上下左右が半径の長さだけ外にはみ出している
CSSCSS · 6行
.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秒)の1/8(0.125秒)ずつずれてドットが動くようになります。

CSSCSS · 15行
.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;  }}

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

ブラウザの対応状況

sin()とcos()は、Chrome 111、Firefox 108、Safari 15.4から対応しています。translateとscaleのプロパティは、それより前から対応しています。

補足

スピナーは見た目だけの要素なので、スクリーンリーダーには「読み込み中」であることが伝わりません。.loading-spinnerにrole="status"とaria-label="読み込み中"を付けておくと、読み上げでも伝わります。

完成品

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

押すと CodePen から読み込みます

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

作成したコード

HTMLHTML · 10行
<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>
CSSCSS · 42行
* {  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;  }}

参考・引用

この記事は役に立ちましたか?