本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100

ローディング

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

デモ

仕様

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

しくみと調整

進み具合が上がるにつれて、輪郭だけで描いた大きな「LOADING」の中が、下から白く満ちていきます。塗りの上端には横線を引き、その両端に工程名と%を乗せました。仕組みは、輪郭の文字の上に白(#FFFFFF)で塗った同じ文字を重ね、clip-path で見せる範囲の上端を下から上へ動かしているだけです。輪郭のほうは文字の色を薄めた線にしてあるので、満ちきったところで白い「LOADING」が浮かび上がります。

塗りが動く範囲は、ベースラインのすぐ下から字の上端のすぐ上まで。ぴったり合わせると、O や G のような丸い文字は、線から少しはみ出している部分が平らに切れてしまいます。上下に少しずつ余白を残したのは、そのためです。

進み具合が飛び飛びに届いても塗りがなめらかに上がるよう、値が変わるたびに0.5秒ほどの power2.out で新しい値まで追いかけています。100%に達したら一呼吸おいてから、塗りを上へ抜いて消します。満ちた状態をすぐに消さないので、読み込みが終わったことが目に残ります。

文言は、HTML の輪郭側だけを書き換えれば足ります。塗り側の文字は、スクリプトが読み込み時に輪郭側を複製して作っているからです。ただし複製するのは最初の1回だけで、動き始めてから輪郭側を書き換えても、塗り側には届きません。塗りの色は CSS 変数の --fill で、文字の大きさは font-size で変えます。

特設サイトやポートフォリオの起動画面のように、ローディングそのものを演出として見せる場面のための表示です。数字だけで足りる場面なら、もっと控えめな表示のほうが読みやすいこともあります。

コード

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 サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。