デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
背景を文字の形だけに透かして見せる background-clip の指定を使います。背景は「地の色・帯・地の色」を並べた 1 枚のグラデーションで、文字幅の 2 倍と帯の幅を足した広さにしておき、背景の位置を右端から左端へ動かすと帯だけが文字の上を通ります。
両端は地の色だけになるようにしてあるので、通り過ぎたあとに繰り返しても継ぎ目が見えません。帯の通過には power1.inOut の ease を使い、動き始めと終わりをゆっくりにして光の帯らしい質感にしています。
背景の外に出た字形は塗られずに消えるので、見出しの余白で内側に収めます。欧文の見出しなら、字の下側のはみ出し(p・y など)の分を下に多めに取ります。帯の色を増やしたり幅を変えたりしても、両端さえ地の色に戻せば繰り返しはそのままつながります。
つまみは帯の色・帯の幅・通過にかかる時間です。帯はラベンダーグレーからくすみローズ、グレージュへ移る 3 色で、明るい文字より少し暗い色を並べると、光るというより色のつやが文字をなでるように見えます。動きは控えめで文字自体は変わらないので、ヒーローの見出しや新機能の告知、価格の強調など、繰り返し見せても飽きにくい場所に向いています。
コード
<section class="shine">
<h1 class="shine__heading">ひとつずつ、<br>磨きあげる。</h1>
</section>
.shine {
--bg: #111111;
--ink: #eeece6;
--band: #b0a5ca, #d29b97, #d2b999; /* 帯の色(左から。ラベンダーグレー・くすみローズ・グレージュ) */
display: grid;
place-items: center;
min-height: min(70vh, 520px);
padding: 96px 6vw;
background: var(--bg);
color: var(--ink);
text-align: center;
}
.shine__heading {
display: inline-block;
margin: 0;
/* 背景の外に出た字形は塗られないので、余白で内側に収める(欧文なら p・y の下がりの分) */
padding: 0.06em 0.06em 0.14em;
font-size: clamp(40px, 8vw, 96px);
font-weight: 800;
line-height: 1.1;
letter-spacing: 0.02em;
}
import { gsap } from "gsap";
const BAND_WIDTH = 0.4; // 帯の幅(文字幅に対する割合)
const SWEEP = 1.8;
const PAUSE = 1.2; // 帯が通り過ぎてから、次に通るまでの間
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".shine__heading").forEach((heading) => {
if (reduceMotion) return; // 動きを減らす設定では、地の色の文字のまま止めておく
const css = getComputedStyle(heading.closest(".shine"));
const base = css.getPropertyValue("--ink").trim();
const BAND = css.getPropertyValue("--band").split(",").map((c) => c.trim()); // 帯の色は CSS の --band に並べる
// 背景は「地の色・帯・地の色」を並べた 1 枚のグラデーション。文字幅の 2 倍 + 帯の幅に広げておく
const total = 2 + BAND_WIDTH;
const from = 100 / total;
const to = ((1 + BAND_WIDTH) * 100) / total;
const stops = BAND.map((color, i) => `${color} ${(from + ((to - from) * (i + 1)) / (BAND.length + 1)).toFixed(2)}%`);
const image = `linear-gradient(90deg, ${base} ${from.toFixed(2)}%, ${stops.join(", ")}, ${base} ${to.toFixed(2)}%)`;
heading.style.backgroundImage = image;
heading.style.backgroundSize = `${(total * 100).toFixed(2)}% 100%`;
heading.style.backgroundRepeat = "no-repeat";
heading.style.backgroundPosition = "100% 0%";
heading.style.webkitBackgroundClip = "text";
heading.style.backgroundClip = "text";
heading.style.color = "transparent";
// background-position を 100% → 0% に動かすと、帯だけが文字の上を通る。両端は地の色だけなので継ぎ目が出ない
gsap.to(heading, { backgroundPosition: "0% 0%", duration: SWEEP, ease: "power1.inOut", repeat: -1, repeatDelay: PAUSE });
});