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

ローディングの数字が 100 に達すると画面が上下に割れて開く

デモ

仕様

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

しくみと調整

画面を上下 2 枚の幕で覆い、どちらにも同じ数字を置いて、境目からはみ出す分を隠します。上の幕には数字の上半分、下の幕には下半分だけが見えるので、読み込みのあいだは 1 つの数字に見えます。数え終わって幕を上下に送ると、数字ごと真っ二つに割れて、下のページが出てきます。

数字は、行の箱の中央ではなく、字の上下の中央を境目に合わせます。欧文の書体は数字の中央が箱の中央より少し上にあるので、箱で合わせると上半分だけが小さくなります。ここでは行の上端から字の大きさの 0.463 倍の位置で、書体を替えたら測り直す値です。

割れる前に、中央へ細い線を 0.35 秒で走らせます。何の前触れもなく割れるより、合図が 1 つあるほうが目が付いていけます。幕は 0.9 秒の power4.inOut で抜けます。前半がゆっくりなので、開き始めの一瞬だけ下のページがのぞき、そこから一気に開きます。

幕を送る量は、画面の高さに対する割合(yPercent)で書きます。px で書くと、窓の高さが変わったときや、スマートフォンでアドレスバーが隠れたときに、開いたはずの幕が帯になって残ります。

進み具合は 0〜1 で渡し、読み込みが終わったときだけ幕を開きます。開く動きを 1 本のタイムラインとして返しておくと、ページ自身の見出しのせり上がりを、幕が開き始める時刻につなげられます。最初の 1 ページだけの演出に向いていて、ページを移るたびに出すと、待ち時間が増えたように感じます。

コード

HTML
<div class="split-loader" role="progressbar" aria-label="読み込みの進み具合"
     aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
  <div class="split-loader__half split-loader__half--top">
    <p class="split-loader__number" aria-hidden="true">0</p>
    <p class="split-loader__note">読み込み中</p>
  </div>

  <!-- 下の幕にも同じ数字を入れる。境目で切り抜かれ、下半分だけが見える -->
  <div class="split-loader__half split-loader__half--bottom">
    <p class="split-loader__number" aria-hidden="true">0</p>
    <p class="split-loader__note">少々お待ちください</p>
  </div>

  <span class="split-loader__line"></span>
</div>
CSS
.split-loader {
  --number: clamp(120px, 26vw, 300px); /* 数字の大きさ */
  --center: 0.463;    /* 字の上下の中央(行の上端からの比)。書体を替えたら測り直す */
  --surface: #262626; /* 幕の色 */
  --ink: #eeece6;     /* 数字と線の色 */
  --mute: #a8a6a0;    /* 待ち時間の文言の色 */
  position: fixed;
  inset: 0;
  z-index: 10;
  color: var(--ink);
}

/* 上下 2 枚の幕。はみ出しを隠すので、境目で数字が切り抜かれる */
.split-loader__half {
  position: absolute;
  left: 0;
  right: 0;
  height: 50%;
  overflow: hidden;
  background: var(--surface);
}

.split-loader__half--top { top: 0; }
.split-loader__half--bottom { bottom: 0; }

/* 行の箱の中央ではなく、字の上下の中央を境目に合わせる */
.split-loader__number {
  position: absolute;
  left: 0;
  right: 0;
  margin: calc(var(--number) * var(--center) * -1) 0 0;
  font-size: var(--number);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.split-loader__half--top .split-loader__number { top: 100%; }
.split-loader__half--bottom .split-loader__number { top: 0; }

.split-loader__note {
  position: absolute;
  left: 32px;
  right: 32px;
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--mute);
}

.split-loader__half--top .split-loader__note { top: 32px; }
.split-loader__half--bottom .split-loader__note { bottom: 32px; text-align: right; }

.split-loader__line {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 2px;
  margin-top: -1px;
  background: var(--ink);
  transform: scaleX(0);
}
JavaScript
import { gsap } from "gsap";

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

function createLoader(root) {
  const halves = root.querySelectorAll(".split-loader__half");
  const numbers = root.querySelectorAll(".split-loader__number");
  const line = root.querySelector(".split-loader__line");
  const state = { value: 0 };

  const draw = () => {
    const text = String(Math.round(state.value));
    numbers.forEach((number) => (number.textContent = text));
    root.setAttribute("aria-valuenow", text);
  };

  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 });
    },

    // 読み込みが終わったら呼ぶ。返ってくるタイムラインに、ページ自身の出現をつなげられる
    // 例: loader.done().add(heroIn, "-=0.75")
    done() {
      return gsap
        .timeline({ onUpdate: draw, onComplete: () => root.remove() })
        // 100 まで数え、中央に線を走らせてから割る
        .to(state, { value: 100, duration: 0.3, ease: "power2.in" })
        .to(line, { scaleX: 1, duration: 0.35, ease: "power3.inOut" })
        // 幕は画面の高さぶん送る。px で送ると、高さが変わったときに幕が残る
        .to(halves[0], { yPercent: -100, duration: 0.9, ease: "power4.inOut" })
        .to(halves[1], { yPercent: 100, duration: 0.9, ease: "power4.inOut" }, "<")
        .to(line, { autoAlpha: 0, duration: 0.25, ease: "power1.out" }, "<0.1")
        .timeScale(SPEED);
    },
  };
}

export const loader = createLoader(document.querySelector(".split-loader"));

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

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