CATEGORIES:CSS / TAGS:
CSSだけでローディングアニメーションを作る#01

画像やJavaScriptを使わず、CSSだけで円が左右に揺れてからジャンプし、着地でつぶれるローディングアニメーションを作る方法。色・高さ・速さは数字の変更だけで調整できる。

動きは1周期1.1秒で、次の順番で繰り返す。

  • 0〜50%:縮まずに、左右へ揺れる
  • 50〜60%:飛び立つ。縦に細長く伸びる
  • 60〜72%:頂点へ。平らに広がり、左に15度傾く
  • 72〜80%:落下。縦に伸びる
  • 80〜88%:着地。横に広がり、縦に強くつぶれる
  • 88〜100%:少し戻って静止

円は ::after、影は ::before で作っているため、HTMLに置くのは <div> 1つだけ。

HTML(表示したい場所に置く)

<div class="loader" role="status" aria-label="読み込み中"></div>
/* 入れ物(円と影の置き場所) */
.loader {
  position: relative;
  width: 20px;          /* 円の直径 */
  height: 24px;         /* 円の高さ(20px) + 影の逃げ(4px) */
  margin-top: 20px;     /* ジャンプの高さと同じにすると頭が切れない */
}

/* 影 */
.loader::before {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 20px;
  height: 5px;
  border-radius: 50%;
  background: #000;
  filter: blur(1.5px);
  opacity: .3;
  animation: loader-shadow 1.1s infinite;  /* 1周期。円と同じ値にする */
}

/* 円 */
.loader::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #1c1d1d;               /* 円の色 */
  transform-origin: center bottom;   /* 傾く・つぶれる基準点は円の下端 */
  animation: loader-jump 1.1s infinite;
}

/* 円の動き:transform は translate → rotate → scale の順に全行そろえる */
@keyframes loader-jump {
  0%   { transform: translate(0, 0)     rotate(0deg)   scale(1, 1);      animation-timing-function: ease-in-out; }
  7%   { transform: translate(3px, 0)   rotate(0deg)   scale(1, 1); }
  19%  { transform: translate(-4px, 0)  rotate(0deg)   scale(1, 1); }
  31%  { transform: translate(4px, 0)   rotate(0deg)   scale(1, 1); }
  44%  { transform: translate(-3px, 0)  rotate(0deg)   scale(1, 1); }
  50%  { transform: translate(0, 0)     rotate(0deg)   scale(1, 1);      animation-timing-function: cubic-bezier(.2, .7, .3, 1); }
  60%  { transform: translate(0, -15px) rotate(-6deg)  scale(.78, 1.3);  animation-timing-function: cubic-bezier(.2, .6, .4, 1); }
  72%  { transform: translate(0, -20px) rotate(-15deg) scale(1.15, .82); animation-timing-function: cubic-bezier(.55, 0, 1, .45); }
  80%  { transform: translate(0, -10px) rotate(-6deg)  scale(.88, 1.18); animation-timing-function: cubic-bezier(.55, 0, 1, .45); }
  88%  { transform: translate(0, 0)     rotate(0deg)   scale(1.3, .55);  animation-timing-function: cubic-bezier(.2, .7, .3, 1); }
  94%  { transform: translate(0, 0)     rotate(0deg)   scale(.94, 1.08); animation-timing-function: ease-in-out; }
  100% { transform: translate(0, 0)     rotate(0deg)   scale(1, 1); }
}

/* 影の動き:高く跳ぶと小さく薄く、着地すると広く濃くなる */
@keyframes loader-shadow {
  0%   { transform: translateX(0)    scaleX(1);    opacity: .3;  animation-timing-function: ease-in-out; }
  7%   { transform: translateX(3px)  scaleX(1); }
  19%  { transform: translateX(-4px) scaleX(1); }
  31%  { transform: translateX(4px)  scaleX(1); }
  44%  { transform: translateX(-3px) scaleX(1); }
  50%  { transform: translateX(0)    scaleX(1);    opacity: .3;  animation-timing-function: cubic-bezier(.2, .7, .3, 1); }
  60%  { transform: translateX(0)    scaleX(.55);  opacity: .14; animation-timing-function: cubic-bezier(.2, .6, .4, 1); }
  72%  { transform: translateX(0)    scaleX(.36);  opacity: .07; animation-timing-function: cubic-bezier(.55, 0, 1, .45); }
  80%  { transform: translateX(0)    scaleX(.65);  opacity: .16; animation-timing-function: cubic-bezier(.55, 0, 1, .45); }
  88%  { transform: translateX(0)    scaleX(1.3);  opacity: .45; animation-timing-function: cubic-bezier(.2, .7, .3, 1); }
  94%  { transform: translateX(0)    scaleX(.98);  opacity: .3; }
  100% { transform: translateX(0)    scaleX(1);    opacity: .3; }
}

/* OSで「視差効果を減らす」設定のときは止める */
@media (prefers-reduced-motion: reduce) {
  .loader::before,
  .loader::after { animation: none; }
}

調整するときの変更箇所は次のとおり。

変えたいもの変更する場所
円の色.loader::after の background
速さanimation の 1.1s(円と影の2か所を同じ値に)
着地のつぶれ方88% の scale(1.3, .55)(.55 を大きくすると弱くなる)
ジャンプの高さmargin-top、60%・72%・80%の translate のY(高さ × 0.75 / 1 / 0.5)
暗い背景で使う影の background を #fff などの明るい色に

ジャンプの高さを変えるときは、@keyframes 内の transform の並び(translate → rotate → scale)を全行で同じ順番にそろえる。順番が違うとなめらかに動かない。

実際のサイトでは、ページの読み込みが終わるまで表示して、完了したらフェードアウトさせる形で使っている。表示環境によって動きの見え方が異なる場合があるため、本番に反映する前に確認を推奨。

NOTICES

  • 記事内容は実装させたものがほとんどですが自己責任で参考にしてください。

TO HEADER