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

ローディングの % に合わせて大きな文字の中が下から満ちる

デモ

仕様

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

しくみと調整

輪郭だけの大きな「LOADING」の上に、白(#FFFFFF)で塗った同じ文字を重ね、clip-path の上端を下から上へ動かして中を満たしていきます。塗りの縁には横線を添え、両端に工程名と % を乗せています。輪郭は文字の色を薄めた線で描き、満ちきったときに「LOADING」が白く浮かぶようにしています。

塗りの上端は、ベースラインのすぐ下から字の上端のすぐ上までの範囲で動かします。ちょうどベースラインや字の上端に合わせると、O や G のような丸い文字のはみ出しが平らに切れてしまうので、少し外側に余白を残しています。塗りの動きは power2.out を基本にし、値が変わるたびに0.5秒ほどで追いかけると、飛び飛びに届く進み具合でもなめらかに見えます。

見出しの文言は輪郭側の要素だけを書き換えれば、複製している塗り側にもそのまま反映されます。工程名は進み具合の節目ごとに切り替えられるので、節目の位置や文言は自由に増やせます。100% に達したら少し間を置いてから塗りを上へ抜くと、読み込みが完了した実感が出ます。

塗りの色や書体の大きさは CSS 変数で差し替えられます。塗りをグラデーションにしたいときは、塗り側の文字に背景を敷き、文字の形に切り抜きます。全画面で見せるローディング演出や、特設サイト・ポートフォリオの起動画面に向いています。数字だけで十分な場面では、もっと控えめな表示のほうが読みやすいこともあります。

コード

HTML
<!-- 読み込みの進み具合。数字と工程名は HTML に書き、JS はそこから読んで動かす -->
<div class="fill-loader">
  <div class="fill-loader__panel" role="progressbar" aria-label="読み込みの進み具合"
       aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
    <p class="fill-loader__word" aria-hidden="true">LOADING</p>
    <p class="fill-loader__row" aria-hidden="true">
      <span class="fill-loader__step">フォントを読み込み中</span>
      <span class="fill-loader__percent"><span class="fill-loader__digits">0</span><span class="fill-loader__unit">%</span></span>
    </p>
  </div>
</div>

CSS
.fill-loader {
  --ink: #eeece6;     /* 輪郭・線・工程名・% の色 */
  --mute: #a8a6a0;    /* % の単位の色 */
  --ground: #111111;  /* 地の色 */
  --fill: #ffffff;    /* 満ちる塗りの色 */
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  background: var(--ground);
  color: var(--ink);
}

/* 塗りの top(JS が書く)の基準にする。行の高さは word の font-size に一致する */
.fill-loader__panel { position: relative; }

.fill-loader__word {
  position: relative;
  margin: 0;
  /* 塗りの縁(script.js の BASE / CAP)は、この書体の並びで測った値。書体を変えたら合わせ直す */
  font-family: "HIGASHIYUKI Font1", "Avenir Next", "Avenir", "Helvetica Neue", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Noto Sans JP", "Yu Gothic", Meiryo, system-ui, sans-serif;
  font-size: clamp(40px, 20vw, 144px);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  white-space: nowrap;
  color: var(--ground);
  -webkit-text-stroke: 0.028em color-mix(in srgb, var(--ink) 30%, transparent);
  paint-order: stroke fill;
}

/* 塗りは輪郭と同じ文字を JS が複製し、塗りの色で塗って clip-path の上端・下端で隠す */
.fill-loader__fill {
  position: absolute;
  inset: 0;
  color: var(--fill);
}

/* 塗りの縁の線。工程名と % を両端に乗せる。top は JS が塗りの上端に合わせて動かす */
.fill-loader__row {
  position: absolute;
  left: 0;
  right: 0;
  top: 85%;
  margin: 0;
  height: 2px;
  background: var(--ink);
}

.fill-loader__step,
.fill-loader__percent {
  position: absolute;
  bottom: 8px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.fill-loader__step { left: 0; }
.fill-loader__percent { right: 0; }
.fill-loader__unit { margin-left: 0.06em; color: var(--mute); }

JavaScript
import { gsap } from "gsap";

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

// 塗りの縁(clip-path の上端・下端 %)。0% はベースラインのすぐ下、100% は字の上端のすぐ上
const BASE = 85;
const CAP = 7.5;
// 進み具合の節目と、そのときの工程名(読む人がここを差し替える)
const STEPS = [
  [0, "フォントを読み込み中"],
  [34, "画像を読み込み中"],
  [68, "シーンを準備中"],
  [100, "準備完了"],
];

function createLoader(root) {
  const panel = root.querySelector(".fill-loader__panel");
  const word = root.querySelector(".fill-loader__word");
  const step = root.querySelector(".fill-loader__step");
  const digits = root.querySelector(".fill-loader__digits");
  const row = root.querySelector(".fill-loader__row");

  // 塗りは輪郭と同じ文字を複製し、塗りの色を付けて clip-path で隠す
  const fill = document.createElement("span");
  fill.className = "fill-loader__fill";
  fill.textContent = word.textContent;
  word.append(fill);

  const state = { value: 0, wipe: 0 };
  const draw = () => {
    const value = Math.round(state.value);
    const top = BASE - ((BASE - CAP) * state.value) / 100;
    const bottom = 100 - (BASE - (BASE - CAP) * state.wipe);
    const label = STEPS.findLast(([from]) => value >= from)[1];
    fill.style.clipPath = `inset(${top}% 0 ${bottom}% 0)`;
    row.style.top = `${top}%`;
    digits.textContent = String(value);
    step.textContent = label;
    panel.setAttribute("aria-valuenow", String(value));
    panel.setAttribute("aria-valuetext", `${value}%・${label}`);
  };
  draw();

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

    // 読み込みが終わったら呼ぶ。100% まで進め、少し置いてから塗りを上へ抜いて消える
    done() {
      return gsap
        .timeline({ onUpdate: draw, onComplete: () => root.remove() })
        .to(state, { value: 100, duration: 0.4 / SPEED, ease: "power2.out" })
        .to(state, { wipe: 1, duration: 0.5 / SPEED, ease: "power2.inOut" }, "+=0.25")
        .to(root, { autoAlpha: 0, duration: 0.3 / SPEED, ease: "power1.in" }, "<0.2");
    },
  };
}

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

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

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