本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ローディング

ローディングの進捗が円形のゲージと % で 0 から 100 に進む

デモ

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

円を 1 本の線で描き、そのうち見えている長さと、始まりの位置だけを書き換えて弧にします。円周を測らずに済むよう、線の長さを 100 として扱わせる指定(pathLength)を円に付けます。すると、進み具合の % をそのまま長さとして書けます。中央の数字は弧とは別の値で、同じ時間に合わせて数え上げます。

進み方は 0 → 24 → 61 → 100 と段に分け、あいだに 0.2 秒ほどの空きを置きます。等速で 100 まで上がると、読み込みの様子ではなく飾りに見えます。100 に届いたら輪を 0.18 秒だけ 1.06 倍に弾ませ、そのあと 0.55 秒かけて弧を尻尾のほうから抜きます。

丸い線端は、長さが 0 でも点として残ります。弧が空のあいだは隠しておかないと、12 時の位置に小さな点が居座ります。見えている長さの指定(stroke-dasharray)を CSS にも書かないよう気をつけます。CSS が勝って、いくら書き換えても弧が動きません。

実際の読み込みに合わせるには、受け取った量を 0〜1 で渡せるようにします。届いた数字をそのまま書き込まず、0.4 秒でその値まで動かすと、飛び飛びで届く進捗でもなめらかに見えます。読み上げには、輪そのものに進み具合の役割と今の値を持たせ、見えている数字のほうは読み上げから外します。

線の太さと輪の大きさは、CSS の値を変えるだけで合わせられます。ファイルの受信や画像の先読みのように、残りが数で分かる待ちに向いています。数が分からないものに当てると、止まったままの弧が故障に見えるので避けます。

コード

HTML
<div class="ring-loader">
  <div class="ring-loader__ring" role="progressbar" aria-label="読み込みの進み具合"
       aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
    <svg class="ring-loader__svg" viewBox="0 0 100 100" aria-hidden="true">
      <defs>
        <linearGradient id="ring-loader-arc" x1="0" y1="0" x2="1" y2="1">
          <stop class="ring-loader__from" offset="0" />
          <stop class="ring-loader__to" offset="1" />
        </linearGradient>
      </defs>
      <circle class="ring-loader__track" cx="50" cy="50" r="42" />
      <!-- 線の長さを 100 として扱わせる。12 時から時計回りに描くため -90° 回す -->
      <circle class="ring-loader__arc" cx="50" cy="50" r="42"
              pathLength="100" transform="rotate(-90 50 50)" />
    </svg>
    <p class="ring-loader__number" aria-hidden="true"><span class="ring-loader__digits">0</span><span class="ring-loader__unit">%</span></p>
  </div>
</div>
CSS
.ring-loader {
  --size: 232px;      /* 輪の直径 */
  --stroke: 8;        /* 線の太さ(viewBox 100 の中の値) */
  --digits: 56px;     /* 数字の大きさ */
  --from: #4f46e5;    /* 弧の色(始まり) */
  --to: #06b6d4;      /* 弧の色(終わり) */
  --ink: #262626;     /* 数字の色 */
  --mute: #6b6b6b;    /* % の色 */
  --line: rgba(38, 38, 38, 0.12); /* 溝の色 */
  --ground: #f2f1ed;  /* 地の色 */
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  background: var(--ground);
  color: var(--ink);
}

.ring-loader__ring {
  position: relative;
  width: var(--size);
  height: var(--size);
}

.ring-loader__svg {
  display: block;
  width: 100%;
  height: 100%;
}

.ring-loader__track {
  fill: none;
  stroke: var(--line);
  stroke-width: var(--stroke);
}

/* 見えている長さは JS が要素の style に書く。ここで dasharray を決めると、CSS が勝って動かない */
.ring-loader__arc {
  fill: none;
  stroke: url(#ring-loader-arc);
  stroke-width: var(--stroke);
  stroke-linecap: round;
}

.ring-loader__from { stop-color: var(--from); }
.ring-loader__to { stop-color: var(--to); }

/* 数字と % は 1 行に置く。並べる箱(flex / grid)にすると、% が下の段へ落ちる */
.ring-loader__number {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  margin: 0;
  transform: translateY(-50%);
  font-size: var(--digits);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.ring-loader__unit {
  margin-left: 0.06em;
  font-size: 0.45em;
  color: var(--mute);
}
JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SPEED = reduceMotion ? 12 : 1; // 動きを減らす設定では、ほぼ一瞬で進める

function createLoader(root) {
  const ring = root.querySelector(".ring-loader__ring");
  const arc = root.querySelector(".ring-loader__arc");
  const digits = root.querySelector(".ring-loader__digits");
  // 弧の頭(head)・尻尾(tail)と、表示する数字(count)。どれも 0〜100
  const state = { head: 0, tail: 0, count: 0 };

  const draw = () => {
    const visible = state.head - state.tail;
    // 長さ 0 でも丸い線端が点で残るので、空のあいだは隠す
    arc.style.visibility = visible > 0.05 ? "" : "hidden";
    // 線の長さを 100 にしてあるので、% をそのまま長さとして書ける
    arc.style.strokeDasharray = `${visible} 100`;
    arc.style.strokeDashoffset = String(-state.tail);
    digits.textContent = String(Math.round(state.count));
    ring.setAttribute("aria-valuenow", String(Math.round(state.count)));
  };

  return {
    // 進み具合を 0〜1 で渡す。飛び飛びで届く値でも、トゥイーンを挟めば滑らかに進む
    set(progress) {
      const to = Math.min(1, Math.max(0, progress)) * 100;
      gsap.to(state, { head: to, count: to, duration: 0.4 / SPEED, ease: "power2.out", onUpdate: draw });
    },

    // 読み込みが終わったら呼ぶ。100% まで進め、弧を尻尾から抜いて消える
    done() {
      return gsap
        .timeline({ onUpdate: draw, onComplete: () => root.remove() })
        .to(state, { head: 100, count: 100, duration: 0.4, ease: "power2.out" })
        .to(ring, { scale: 1.06, duration: 0.18, ease: "power2.out", yoyo: true, repeat: 1 })
        .to(state, { tail: 100, duration: 0.55, ease: "power2.inOut" })
        .to(root, { autoAlpha: 0, duration: 0.3, ease: "power1.in" }, "<0.2")
        .timeScale(SPEED);
    },
  };
}

// 例: 受け取った量を loader.set(loaded / total) で渡し、読み終わったら loader.done()
export const loader = createLoader(document.querySelector(".ring-loader"));

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。