デモ
デモを読み込めませんでした。
仕様
- 用途
- ボタン
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
ボタンの左の外に、斜めに傾けた光の帯を隠しておき、カーソルが重なったら右の外まで流します。帯は中央だけが淡いグレージュに光り、両端が透明なグラデーションで、幅はボタンの半分(50%)です。ボタンは、はみ出しを隠した丸帯なので、光は丸帯の形に切り取られて見えます。
帯は skewX で -20° 傾け、1.155 秒の power2.inOut で流します。傾けた帯は、ボタンの高さ × tan 20° だけ角が横へはみ出すので、その分だけ流し始めと流し終わりの位置を外へずらします。ずらさないと、流す前と流し終えたあとに、帯の角がボタンの端にのぞきます。
帯は文字より下に置きます。上に重ねると、帯が通るあいだ白い文字が光に溶けて読めなくなります。下に回すために z-index を -1 にするときは、ボタンに isolation の isolate を付けます。付けないと、帯がボタンの地より後ろへ回り、何も見えなくなります。
光は重なるたびに 1 回だけ流し、離れるときは何もしません。流れている途中にまた重なっても、頭から流し直さずに流しきらせます。流し直すと、帯が途中から左の外へ飛んで見えるためです。キーボードで焦点が来たときも 1 回流し、動きを減らす設定では、流さずに真ん中で淡く光らせて消すだけにします。
明るい光は暗い面の上でよく見えるので、墨色の主ボタンに向いています。光の色は白でもよく、グレージュにすると金属の照り返しのような落ち着いた光になります。明るいボタンでは帯が地に溶けるので、墨色の帯を薄く流します。重ねなくても数秒おきに流せば(repeatDelay を付けた繰り返し)、目を留めてほしいボタンになります。有料プランへの誘導や購入のボタンに向いています。
コード
<a class="shine-button" href="/pricing/">
<span class="shine-button__shine" aria-hidden="true"></span>
<span class="shine-button__label">プランをアップグレード</span>
</a>
.shine-button {
--face: #262626; /* 地 */
--on-face: #ffffff; /* 文字 */
--shine: rgba(210, 185, 153, 0.8); /* 光の帯の芯(淡いグレージュ)。両端は透明 */
position: relative;
display: inline-grid;
place-items: center;
height: 48px;
padding: 0 32px;
border-radius: 999px;
overflow: hidden; /* 光の帯を丸帯の形に切る */
isolation: isolate; /* 帯(z-index: -1)を、ボタンの地の上・文字の下に描く */
background-color: var(--face);
color: var(--on-face);
font-size: 15px;
font-weight: 600;
letter-spacing: 0.04em;
text-decoration: none;
white-space: nowrap;
}
/* 光の帯。幅はボタンの 50%。ボタンの左の外に置いておき、重なったら右の外まで流す */
.shine-button__shine {
position: absolute;
top: 0;
right: 100%;
z-index: -1;
width: 50%;
height: 100%;
background: linear-gradient(90deg, transparent, var(--shine), transparent);
pointer-events: none;
}
.shine-button:focus-visible {
outline: 2px solid var(--face);
outline-offset: 4px;
}
import { gsap } from "gsap";
const SKEW = -20; // 光の帯の傾き(度)
const TILT = Math.tan((Math.abs(SKEW) * Math.PI) / 180);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".shine-button").forEach((button) => {
const shine = button.querySelector(".shine-button__shine");
// 傾けた帯は、高さ × tan(傾き) だけ角が横へはみ出す。休みのときも、その分だけ外へずらしておく
const slant = () => button.offsetHeight * TILT;
gsap.set(shine, { skewX: SKEW, x: -slant() });
let sweep = null;
const shineOnce = () => {
// 流れている途中は頭から流し直さない(帯が途中から左の外へ飛んで見える)
if (sweep?.isActive()) return;
// 大きさはその都度測る(書体が読み込まれると、ボタンの幅が変わる)
const width = button.offsetWidth;
const band = shine.offsetWidth;
// 動きを減らす設定では流さず、真ん中で淡く光らせて消すだけにする
if (reduceMotion) {
sweep = gsap.fromTo(shine, { x: (width + band) / 2, opacity: 1 }, { opacity: 0, duration: 0.66, ease: "power1.out" });
return;
}
// はみ出す分も含めて、外から外へ流す
sweep = gsap.fromTo(shine, { x: -slant() }, { x: width + band + slant(), duration: 1.155, ease: "power2.inOut" });
};
// 重なるたびに 1 回だけ流す。離れるときは何もしない
button.addEventListener("pointerenter", shineOnce);
// キーボードの焦点のときだけ流す(マウスで押したときの焦点では流さない)
button.addEventListener("focus", () => {
if (button.matches(":focus-visible")) shineOnce();
});
});