デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
文の途中の語だけが、ドラムが回るように奥へ倒れて、次の語に入れ替わります。入れ替わるたびに、その語を入れた要素の幅も次の語の幅へなめらかに変わるので、後ろに続く文が押し広げられたり、引き寄せられたりします。文の言い回しはそのままで語だけを入れ替えていけるので、ヒーローのキャッチコピーで、提供する価値をいくつも順に見せたいときの形です。
語はすべて1つの要素の中に重ね、はみ出した部分は切り取って隠しています。回転の軸は語の奥に置きました。文字が正面を向いたまま消えるのではなく、奥へ倒れ込むように消えていくのは、この軸の位置によるものです。1つの語を1.3秒読ませ、0.7秒の power3.inOut で次へ切り替えます。
スクリーンリーダーには、文全体を固定の文言として渡し、回っている語そのものは対象から外しています。回る語をそのまま渡すと、切り替わるたびに読み上げが割り込み、肝心の文が聞き取れなくなります。
読ませる時間と回転の速さのほかに、回転軸の奥行きも変えられます。浅くすると語はその場で前後に倒れるように回り、深くすると大きなドラムの面に沿って、上下へ移動しながら回ります。
コード
<div class="word-rotator">
<h2 class="word-rotator__heading" aria-label="もっと速い、美しい、使いやすいサイトを。">
<span aria-hidden="true">もっと<span class="word-rotator__slot">
<span class="word-rotator__word">速い</span>
<span class="word-rotator__word">美しい</span>
<span class="word-rotator__word">使いやすい</span>
</span>サイトを。</span>
</h2>
<button class="word-rotator__toggle" type="button" aria-pressed="false">一時停止</button>
</div>
.word-rotator {
display: grid;
justify-items: center;
gap: 24px;
padding: 96px 6vw;
background: #f2f1ed;
color: #262626;
text-align: center;
}
.word-rotator__heading {
margin: 0;
font-size: clamp(22px, 5.5vw, 64px);
font-weight: 800;
line-height: 1.2;
letter-spacing: -0.02em;
}
.word-rotator__slot {
display: inline-block;
position: relative;
vertical-align: top;
width: 5em;
height: 1.2em;
overflow: hidden;
perspective: 220px;
}
.word-rotator__word {
position: absolute;
left: 0;
top: 0;
line-height: 1.2;
white-space: nowrap;
opacity: 0;
backface-visibility: hidden;
}
/* JS が動くまでの既定は、最初の語だけ見せておく */
.word-rotator__word:first-child { opacity: 1; }
.word-rotator__word:nth-child(1) { color: #4a5b66; }
.word-rotator__word:nth-child(2) { color: #b27771; }
.word-rotator__word:nth-child(3) { color: #739374; }
.word-rotator__toggle {
height: 40px;
padding: 0 20px;
border: 0;
border-radius: 999px;
box-shadow: inset 0 0 0 1px #262626;
background: transparent;
color: #262626;
font: inherit;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.04em;
cursor: pointer;
}
.word-rotator__toggle[aria-pressed="true"] { background: #262626; color: #f2f1ed; }
.word-rotator__toggle:focus-visible { outline: 2px solid #262626; outline-offset: 4px; }
import { gsap } from "gsap";
const HOLD = 1.3; // 語を読ませる時間
const TURN = 0.7; // 回って切り替わる時間
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".word-rotator").forEach((root) => {
const slot = root.querySelector(".word-rotator__slot");
const words = [...slot.querySelectorAll(".word-rotator__word")];
const toggle = root.querySelector(".word-rotator__toggle");
// 語の幅は字形で決まるので、フォントが確定してから測る
document.fonts.ready.then(() => {
const widths = words.map((word) => word.offsetWidth);
gsap.set(words, { transformOrigin: "50% 50% -60px", rotationX: -90, opacity: 0 });
gsap.set(words[0], { rotationX: 0, opacity: 1 });
gsap.set(slot, { width: widths[0] });
const turn = { duration: TURN, ease: "power3.inOut" };
const tl = gsap.timeline({ repeat: -1 });
words.forEach((word, i) => {
const next = words[(i + 1) % words.length];
tl.to(word, { rotationX: 90, opacity: 0, ...turn }, `+=${HOLD}`)
.fromTo(next, { rotationX: -90, opacity: 0 }, { rotationX: 0, opacity: 1, ...turn }, "<")
.to(slot, { width: widths[(i + 1) % words.length], ...turn }, "<");
});
// 動きを減らす設定では止めず、切り替えをほぼ一瞬にする(読ませる間だけは残す)
if (reduceMotion) tl.timeScale(20);
toggle.addEventListener("click", () => {
const paused = !tl.paused();
tl.paused(paused);
toggle.setAttribute("aria-pressed", String(paused));
});
});
});