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

ローディングが終わると幕が上がってページが現れる

デモ

仕様

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

しくみと調整

読み込み中の画面の下に、オリーブグレーの色の幕をもう 1 枚重ねておきます。読み込みが終わったら、進み具合の数字と帯を消し、読み込み中の画面と色の幕を、同じイージングのまま少しずらして上へ抜きます。ずらした分だけ 2 枚の間に色の帯が走り、幕が上がったことがはっきり伝わります。幕が通り過ぎたところで、ページの見出しが行ごとにマスクの下からせり上がります。

2 枚はどちらも 1 秒の power3.inOut で上げ、色の幕を 0.1 秒遅らせます。遅れを大きくすると色の帯が太くなり、幕の印象が強まります。見出しは、色の幕が動き出して 0.52 秒後から、下の行を先に 0.08 秒ずつずらし、0.9 秒の expo.out でせり上げます。幕は下から開くので、下の行から出すと、縁が通り過ぎた順に文字が現れます。

見出しの行を包むマスクを行の箱ぴったりにすると、欧文の g や y のように下へはみ出す字の端が切れて、別の字に読めます。和文の見出しにも英字や数字は混ざるので、マスクの下に字の大きさの 0.06 倍ほどの余白を足し、同じだけ負の余白(margin-bottom)で詰めて、行の間隔を保ちます。

実際の読み込みに合わせて動かすため、進み具合を 0〜1 で渡す set と、読み込みの終わりを知らせる done を用意します。渡した値へは 0.4 秒で追いかけるので、飛び飛びに届く進み具合でもなめらかに進みます。終わりを知らせると 100 まで進めてから幕を上げ、返すタイムラインにページ自身の出現をつなげられます。進み具合は帯に進捗の役割と今の値を持たせて読み上げに届け、大きな数字は読み上げから外します。

動きを減らす設定では、幕を動かさずに薄くして消します。つまみは、幕の色と遅れ、上げる時間です。読み込みがすぐ終わるページでは、幕だけが一瞬よぎって騒がしく見えるので、画像の多いポートフォリオや、最初に大きな画像を読むページのように、待ち時間が実際にある所に向いています。

コード

HTML
<!-- 読み込み中の幕。ページの上に重ねておき、読み込みが終わったら上へ抜ける -->
<div class="curtain">
  <div class="curtain__accent"></div>
  <div class="curtain__panel">
    <div class="curtain__content">
      <div class="curtain__row">
        <p class="curtain__label">読み込み中</p>
        <p class="curtain__count" aria-hidden="true"><span class="curtain__digits">0</span><span class="curtain__unit">%</span></p>
      </div>
      <div class="curtain__track" role="progressbar" aria-label="読み込みの進み具合"
           aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
        <div class="curtain__bar"></div>
      </div>
    </div>
  </div>
</div>

<main class="page">
  <!-- 見出しは行ごとにマスクで包み、幕が上がったあと下の行からせり上げる -->
  <h1 class="page__title">
    <span class="page__line"><span class="page__text">心が動く、</span></span>
    <span class="page__line"><span class="page__text">ものづくり。</span></span>
  </h1>
  <p class="page__lead">ここに説明文が入ります。</p>
  <a class="page__button" href="/work/">制作実績を見る</a>
</main>

CSS
.curtain {
  --panel: #262626;                  /* 読み込み中の幕 */
  --accent: #c8bf83;                 /* 追いかける色の幕 */
  --ink: #eeece6;                    /* 数字と進み具合の帯 */
  --mute: #898989;                   /* % */
  --mute-small: #a8a6a0;             /* 小さい文言 */
  --line: rgba(238, 236, 230, 0.14); /* 帯の溝 */
  position: fixed;
  inset: 0;
  z-index: 10;
}

/* 下に色の幕、上に読み込み中の幕。どちらも画面の高さぶん上へ送って抜く */
.curtain__accent,
.curtain__panel { position: absolute; inset: 0; }
.curtain__accent { background: var(--accent); }

.curtain__panel {
  display: grid;
  align-content: center;
  padding: 0 clamp(24px, 7vw, 96px);
  background: var(--panel);
  color: var(--ink);
}

.curtain__row { display: flex; justify-content: space-between; align-items: flex-end; gap: 24px; }
.curtain__label { margin: 0 0 10px; color: var(--mute-small); font-size: 13px; font-weight: 600; letter-spacing: 0.06em; }

/* 数字と % は 1 行に置く(flex や grid で並べると、% が下の段へ落ちる) */
.curtain__count {
  margin: 0;
  font-size: clamp(48px, 8vw, 72px);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.curtain__unit { margin-left: 4px; color: var(--mute); font-size: 0.4em; }
.curtain__track { height: 4px; margin-top: 16px; overflow: hidden; background: var(--line); }
.curtain__bar { height: 100%; background: var(--ink); transform: scaleX(0); transform-origin: 0 50%; }

/* 幕の下のページ */
.page {
  --bg: #111111;
  --ink: #eeece6;
  min-height: 100vh;
  box-sizing: border-box;
  padding: 200px clamp(24px, 7vw, 96px) 96px;
  background: var(--bg);
  color: var(--ink);
}

.page__title { margin: 0; font-size: clamp(36px, 7vw, 72px); font-weight: 800; line-height: 1.05; letter-spacing: -0.02em; }

/* 行を包むマスク。下に余白を足し、欧文の g や y の下が切れないようにする(同じだけ負の余白で行の間隔を保つ) */
.page__line { display: block; overflow: hidden; padding-bottom: 0.06em; margin-bottom: -0.06em; }
.page__text { display: block; }

.page__lead { max-width: 34em; margin: 32px 0 0; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; word-break: auto-phrase; }

.page__button {
  display: inline-block;
  margin-top: 40px;
  padding: 0 32px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--bg);
  font-size: 14px;
  font-weight: 600;
  line-height: 48px;
  letter-spacing: 0.04em;
  text-decoration: none;
}

.page__button:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const LIFT = 1; // 幕が上がる時間(秒)
const LAG = 0.1; // 色の幕の遅れ。大きいほど、2 枚の間の色の帯が太くなる

function createLoader(root) {
  const accent = root.querySelector(".curtain__accent");
  const panel = root.querySelector(".curtain__panel");
  const content = root.querySelector(".curtain__content");
  const track = root.querySelector(".curtain__track");
  const bar = root.querySelector(".curtain__bar");
  const digits = root.querySelector(".curtain__digits");
  const state = { value: 0 };
  let ending = false;

  const draw = () => {
    const value = String(Math.round(state.value));
    bar.style.transform = `scaleX(${state.value / 100})`;
    digits.textContent = value;
    track.setAttribute("aria-valuenow", value);
  };

  return {
    // 進み具合を 0〜1 で渡す。飛び飛びに届いても、0.4 秒で追いかけるのでなめらかに進む
    set(progress) {
      if (ending) return; // 幕を上げ始めたら、遅れて届いた値で戻さない
      const to = Math.min(1, Math.max(0, progress)) * 100;
      gsap.to(state, { value: to, duration: 0.4, ease: "power2.out", onUpdate: draw, overwrite: true });
    },

    // 読み込みが終わったら呼ぶ。100 まで進め、中身を消してから、2 枚の幕を同じイージングで少しずらして上げる。
    // 返ってくるタイムラインに、ページ自身の出現をつなげられる
    done() {
      ending = true;
      const tl = gsap
        .timeline({ onComplete: () => root.remove() })
        .to(state, { value: 100, duration: 0.35, ease: "power2.in", onUpdate: draw, overwrite: true });
      // 動きを減らす設定では、幕を動かさずに薄くして消す
      if (reduceMotion) return tl.to(root, { autoAlpha: 0, duration: 0.3, ease: "power1.out" });
      return tl
        .to(content, { autoAlpha: 0, y: -24, duration: 0.35, ease: "power2.in" }, "+=0.1")
        .to(panel, { yPercent: -100, duration: LIFT, ease: "power3.inOut" })
        .to(accent, { yPercent: -100, duration: LIFT, ease: "power3.inOut" }, `<${LAG}`);
    },
  };
}

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

// 例: ページの画像が読めるたびに進め、すべて読み終えたら(load)幕を上げて見出しを出す。
// loading="lazy" の画像は画面に入るまで読まれないので、数に入れない
const images = [...document.images].filter((image) => image.loading !== "lazy");
let loaded = 0;
images.forEach((image) => image.decode().catch(() => {}).then(() => loader.set(++loaded / images.length)));
addEventListener("load", () => {
  const tl = loader.done();
  // 見出しは、幕の縁が行を通り過ぎるころから、下の行を先にせり上げる(幕は下から開くため)
  if (!reduceMotion) tl.from(".page__text", { yPercent: 110, duration: 0.9, ease: "expo.out", stagger: { each: 0.08, from: "end" } }, "<0.52");
}, { once: true });

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

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