デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
輪郭だけの大きな「LOADING」の上に、白(#FFFFFF)で塗った同じ文字を重ね、clip-path の上端を下から上へ動かして中を満たしていきます。塗りの縁には横線を添え、両端に工程名と % を乗せています。輪郭は文字の色を薄めた線で描き、満ちきったときに「LOADING」が白く浮かぶようにしています。
塗りの上端は、ベースラインのすぐ下から字の上端のすぐ上までの範囲で動かします。ちょうどベースラインや字の上端に合わせると、O や G のような丸い文字のはみ出しが平らに切れてしまうので、少し外側に余白を残しています。塗りの動きは power2.out を基本にし、値が変わるたびに0.5秒ほどで追いかけると、飛び飛びに届く進み具合でもなめらかに見えます。
見出しの文言は輪郭側の要素だけを書き換えれば、複製している塗り側にもそのまま反映されます。工程名は進み具合の節目ごとに切り替えられるので、節目の位置や文言は自由に増やせます。100% に達したら少し間を置いてから塗りを上へ抜くと、読み込みが完了した実感が出ます。
塗りの色や書体の大きさは CSS 変数で差し替えられます。塗りをグラデーションにしたいときは、塗り側の文字に背景を敷き、文字の形に切り抜きます。全画面で見せるローディング演出や、特設サイト・ポートフォリオの起動画面に向いています。数字だけで十分な場面では、もっと控えめな表示のほうが読みやすいこともあります。
コード
<!-- 読み込みの進み具合。数字と工程名は 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>
.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); }
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"));