デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
縦に並べた棒を、左から右へ少しずつ遅らせながら伸び縮みさせ、音量計のような波を作ります。往復の指定はstaggerの中にまとめて書くので、外に書いたときのように全部の棒がまとめて上下することはありません。
1本が伸びて縮むまでは1.08秒、隣の棒とは0.09秒ずらします。縮みきった高さは最大の28%で、丸くなだらかなイージングを使うので、山と谷の両方が柔らかく見えます。
読み込みが終わる合図を受け取る口をあらかじめ用意しておき、そこで初めて波を止めて要素を取り除きます。合図が来るまで棒は動き続けるので、待ち時間の長さが分からない処理でも安心して使えます。動きを減らす設定では、縮みきった高さを最大近くまで引き上げ、動きをごく小さくします。
棒の数を増やしても、ずらす間隔を変えるだけで同じ形を保てます。音声の入力レベルや、生成中・解析中の待ち時間を表す画面に向いています。
コード
<div class="bars-loader" role="status">
<span class="bars-loader__bar"></span>
<span class="bars-loader__bar"></span>
<span class="bars-loader__bar"></span>
<span class="bars-loader__bar"></span>
<span class="bars-loader__bar"></span>
<span class="bars-loader__bar"></span>
<span class="bars-loader__bar"></span>
<span class="bars-loader__bar"></span>
<span class="bars-loader__bar"></span>
<span class="bars-loader__text">読み込み中</span>
</div>
.bars-loader {
--ground: #111111; /* 地の色 */
--gap: 10px; /* 棒の間 */
--width: 10px; /* 棒の幅 */
--height: 64px; /* 棒の最大の高さ */
position: fixed;
inset: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
gap: var(--gap);
background: var(--ground);
}
.bars-loader__bar {
width: var(--width);
height: var(--height);
border-radius: calc(var(--width) / 2);
transform-origin: center;
}
/* 中身の色の墨のスレート → スレート → グレージュへ、左から右へ少しずつ明るくする。中身の色なので CSS 側に固定で置く */
.bars-loader__bar:nth-child(1) { background: #668fa9; box-shadow: 0 0 16px #668fa955; }
.bars-loader__bar:nth-child(2) { background: #7097b3; box-shadow: 0 0 16px #7097b355; }
.bars-loader__bar:nth-child(3) { background: #79a0bd; box-shadow: 0 0 16px #79a0bd55; }
.bars-loader__bar:nth-child(4) { background: #82aac7; box-shadow: 0 0 16px #82aac755; }
.bars-loader__bar:nth-child(5) { background: #8cb3d0; box-shadow: 0 0 16px #8cb3d055; }
.bars-loader__bar:nth-child(6) { background: #9ab7c5; box-shadow: 0 0 16px #9ab7c555; }
.bars-loader__bar:nth-child(7) { background: #c1c1a2; box-shadow: 0 0 16px #c1c1a255; }
.bars-loader__bar:nth-child(8) { background: #cfb999; box-shadow: 0 0 16px #cfb99955; }
.bars-loader__bar:nth-child(9) { background: #ddba8e; box-shadow: 0 0 16px #ddba8e55; }
/* 読み上げにだけ届く文言。画面には出さない */
.bars-loader__text {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
import { gsap } from "gsap";
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const PERIOD = 1.08; // 1 本が伸びて縮むまで(秒)
const EACH = 0.09; // 隣の棒とのずれ
// 動きを減らす設定では、ほぼ動かさずに縮みきった高さを最大に近づける
const LOW = reduceMotion ? 0.85 : 0.28;
function createLoader(root) {
const bars = root.querySelectorAll(".bars-loader__bar");
// yoyo と repeat は stagger の中に書く(外に書くと、全部の棒がまとめて往復する)
const wave = gsap.fromTo(
bars,
{ scaleY: LOW },
{ scaleY: 1, duration: PERIOD / 2, ease: "sine.inOut", stagger: { each: EACH, repeat: -1, yoyo: true } }
);
return {
// 読み込みが終わったら呼ぶ。呼ぶまで波打ち続ける
done() {
gsap.to(root, {
autoAlpha: 0,
duration: reduceMotion ? 0 : 0.3,
onComplete: () => {
wave.kill();
root.remove();
},
});
},
};
}
// 例: fetch や画像の読み込みを待って loader.done()
export const loader = createLoader(document.querySelector(".bars-loader"));