デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
ラベンダーからローズカラー、グレージュへと色の移る光の筋が、見出しの上を左から右へゆっくり横切っていきます。筋の色は、明るい文字の色より少しだけ暗くしました。光らせるというより、色のつやが文字の表面をなでていくように見せたかったからです。
仕組みは background-clip の text 指定で、背景を文字の形に切り抜いています。背景は「文字の色・光の筋・文字の色」を並べた1枚のグラデーションで、幅を文字幅の2倍に筋の幅を足した広さにしてあり、background-position を100%から0%へ動かすと、筋だけが文字の上を通り抜けます。両端はふだんの文字の色なので、通り過ぎれば元の見出しに戻り、繰り返しても継ぎ目は見えません。筋の色を増やしても幅を変えても、両端さえ文字の色にしておけば、このつながりは崩れません。
動きは power1.inOut で、通り始めと抜ける間際をゆっくりにしています。注意がいるのは字形のはみ出しで、背景の外に出た部分は塗られずに消えてしまいます。見出しには余白をとって字形を背景の内側に収め、欧文なら p や y のように下へ伸びる部分のぶん、下の余白を多めにとってください。文字そのものは変わらず動きも控えめなので、ヒーローの見出しや新機能の告知、価格の強調のように、何度も目に入る場所で繰り返し流しても邪魔になりません。
コード
<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 });
});