デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
字間を広く薄く置いた見出しが、字間を詰めながらはっきり現れます。letterSpacing を em のままトゥイーンし、字間は末尾の文字の後ろにも付くので、同じ量を padding-left にも入れて中央をそろえます。
字間は 0.5em から 0.04em へ 2.4 秒の expo.out で詰め、不透明度はそれより短い 1.2 秒で先に上げきります。小見出しは 0.14em の字間の英大文字ラベルです。
字間が変わると行の幅も変わるので、white-space: nowrap で 1 行に収め、詰まる途中で折り返しが変わって跳ねないようにします。
詰める速さや開始の字間を変えると、映画のタイトルのような重さの違いが出ます。動きを減らす設定では、詰まりきった状態のまま見せます。ブランドサイトのヒーローや、章の扉に向いています。
コード
<section class="wide-title">
<p class="wide-title__eyebrow">Introducing</p>
<h2 class="wide-title__heading">物語は、次の章へ。</h2>
</section>
.wide-title {
--bg: #111111;
--ink: #eeece6;
--mute: #a8a6a0;
display: grid;
place-items: center;
align-content: center; /* これが無いと、2 段の行が高さいっぱいに伸びて間に大きな隙間ができる */
min-height: 100vh;
padding: 96px 6vw;
overflow-x: hidden; /* 出始めは字間が広く、狭い画面では見出しが画面幅を超えるため */
background: var(--bg);
color: var(--ink);
text-align: center;
}
.wide-title__eyebrow {
margin: 0 0 32px;
padding-left: 0.14em; /* 字間ぶんを左にも入れて中央をそろえる */
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--mute);
}
.wide-title__heading {
margin: 0;
font-size: clamp(32px, 6vw, 72px);
font-weight: 800;
line-height: 1;
white-space: nowrap;
}
import { gsap } from "gsap";
const WIDE = "0.5em"; // 出始めの字間
const TIGHT = "0.04em"; // 落ち着いたときの字間
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".wide-title").forEach((section) => {
const heading = section.querySelector(".wide-title__heading");
const eyebrow = section.querySelector(".wide-title__eyebrow");
// 字間は末尾の文字の後ろにも付くので、同じ値を padding-left にも入れて中央をそろえる
const reveal = gsap
.timeline({ paused: true })
.fromTo(
heading,
{ letterSpacing: WIDE, paddingLeft: WIDE, opacity: 0 },
{ letterSpacing: TIGHT, paddingLeft: TIGHT, opacity: 1, duration: 2.4, ease: "expo.out" }
)
.fromTo(eyebrow, { opacity: 0, y: 12 }, { opacity: 1, y: 0, duration: 0.8, ease: "power2.out", force3D: false }, 0.7);
// 動きを減らす設定では、出きった状態のまま置く
if (reduceMotion) {
reveal.progress(1);
return;
}
// 画面に入ったら 1 回だけ流す
const io = new IntersectionObserver(
(entries, self) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
self.unobserve(entry.target);
reveal.play();
}
},
{ rootMargin: "0px 0px -15%" }
);
io.observe(section);
});