本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ボタン

ボタンの枠のグラデーションが回り続け、ホバーで光が強まる

デモ

操作:カーソルを重ねる。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

用途
ボタン
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

ボタンは 3 枚重ねです。いちばん後ろにグロー、その上に枠、いちばん手前に 2px 内側の地(文字と矢印)を置きます。枠は円錐グラデーション(conic-gradient)で描いた光の筋 2 本で、頭がいちばん明るく、尾へ向けて透明になります。2 本目は 180° ずらして色を変え、筋の無い所にはうっすらした縁をいつも見せておきます。

回すのは CSS 変数 --spin の角度だけで、要素そのものは回しません。GSAP で 0° → 360° を 2.6 秒で繰り返し、ホバー中は timeScale を 0.5 秒かけて 2.5 倍まで上げます。同時にグローの濃さを 0.28 から 1 へ上げ、地を少し明るくして、矢印を 5px 進めます。離れるときは 0.7 秒かけてゆっくり戻します。速さを CSS のアニメーションの長さで変えると角度が飛びますが、timeScale なら今の角度のまま滑らかに加速します。

グローは filter: blur を使わず、頭の先まで滑らかに消した別の筋を、ボタンより 24px 大きいピルに敷き、放射状の mask で縁の近くだけを光らせます。枠と同じ頭の切れた筋をそのままぼかすと、中心から放射する硬い楔のように見えます。グローは z-index の負の値でボタンの後ろに回すので、ボタンに isolation: isolate を付けます。付けないと、カードなど親の地の後ろへ潜って見えなくなります。

回り続ける動きなので、動きを減らす設定では回さず、筋を止めた状態で見せます。画面の外にあるあいだも止めて、見えない所で描き直さないようにします。キーボードで焦点が来たときも、ホバーと同じように光らせます。筋の色と長さ(150°)、回る速さ、グローの濃さで印象が変わります。暗い地で使う前提で、料金表のおすすめのプラン、暗いヒーローの主なボタン、AI 系のサービスの「試す」ボタンに向いています。

コード

HTML
<!-- 押すと移るので a。回る光の筋とグローは、style.css の疑似要素で描く -->
<a class="glow-button" href="/pricing/pro/">
  <span class="glow-button__label">
    プロにアップグレード
    <svg class="glow-button__arrow" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
      <path d="M5 12h13M13 6.5l5.5 5.5-5.5 5.5" />
    </svg>
  </span>
</a>

CSS
.glow-button {
  --bg: #111111;                     /* 中の地(暗い地で使う) */
  --bg-hover: #202020;               /* ホバー中の地 */
  --ink: #eeece6;                    /* 文字と矢印 */
  --line: rgba(238, 236, 230, 0.14); /* 筋の無い所にうっすら見せる縁 */
  --glow: 0.28;                      /* グローの濃さ。ホバーで 1 へ(script.js) */
  --tail: 150deg;                    /* 光の筋の長さ */
  --spin: 0deg;                      /* 筋の角度。script.js が回す */
  /* 光の筋の色(尾 → 中 → 頭)。2 本目は反対側を回る */
  --a-tail: #b0a5ca;
  --a-mid: #d29b97;
  --a-head: #e6d4ce;
  --b-tail: #98b1c4;
  --b-mid: #a5c0a3;
  --b-head: #d8dfd2;
  position: relative;
  isolation: isolate; /* 疑似要素を z-index の負の値で後ろに回しても、ページの地より手前に留める */
  display: inline-grid;
  padding: 2px;       /* 枠の太さ */
  border-radius: 999px;
  color: var(--ink);
  text-decoration: none;
}

/* グロー: ひと回り大きいピルに、頭の先も滑らかに消した筋を敷き、mask で縁の近くだけを光らせる */
.glow-button::before {
  content: "";
  position: absolute;
  inset: -24px;
  z-index: -2;
  border-radius: 999px;
  background:
    conic-gradient(from calc(var(--spin) + 18deg), transparent 0deg calc(360deg - var(--tail)), var(--a-tail) calc(360deg - var(--tail) * 0.4), var(--a-mid) calc(342deg - var(--tail) * 0.05), transparent 360deg),
    conic-gradient(from calc(var(--spin) + 198deg), transparent 0deg calc(360deg - var(--tail)), var(--b-tail) calc(360deg - var(--tail) * 0.4), var(--b-mid) calc(342deg - var(--tail) * 0.05), transparent 360deg);
  -webkit-mask-image: radial-gradient(closest-side, #000 50%, rgba(0, 0, 0, 0.45) 78%, transparent 100%);
  mask-image: radial-gradient(closest-side, #000 50%, rgba(0, 0, 0, 0.45) 78%, transparent 100%);
  opacity: var(--glow);
  pointer-events: none; /* 外へ広がる光に重なっただけでは反応させない */
}

/* 枠: 頭がいちばん明るく、尾へ向けて透明になる筋 2 本と、うっすらした常設の縁 */
.glow-button::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background:
    conic-gradient(from var(--spin), transparent 0deg calc(360deg - var(--tail)), var(--a-tail) calc(360deg - var(--tail) * 0.45), var(--a-mid) calc(360deg - var(--tail) * 0.1), var(--a-head) 359deg, transparent 359.6deg),
    conic-gradient(from calc(var(--spin) + 180deg), transparent 0deg calc(360deg - var(--tail)), var(--b-tail) calc(360deg - var(--tail) * 0.45), var(--b-mid) calc(360deg - var(--tail) * 0.1), var(--b-head) 359deg, transparent 359.6deg),
    linear-gradient(var(--line), var(--line));
}

.glow-button__label {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 52px;
  padding: 0 30px;
  border-radius: 999px;
  background-color: var(--bg);
  font-size: 15px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.glow-button__arrow {
  flex: none;
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.glow-button:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

JavaScript
import { gsap } from "gsap";

const LAP = 2.6; // 1 周の秒数(ホバーしていないとき)
const BOOST = 2.5; // ホバー中の回る速さの倍率
const NUDGE = 5; // ホバーで矢印を進める量(px)
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".glow-button").forEach((button) => {
  const label = button.querySelector(".glow-button__label");
  const arrow = button.querySelector(".glow-button__arrow");
  const styles = getComputedStyle(button);
  // 色と濃さは CSS が正。GSAP は var(--name) のままでは補間できないので、値にして使う
  const bg = styles.getPropertyValue("--bg").trim();
  const bgHover = styles.getPropertyValue("--bg-hover").trim();
  const rest = parseFloat(styles.getPropertyValue("--glow"));

  // 角度だけを回し続ける(CSS 変数は始点が読めないことがあるので、fromTo で両端を渡す)。
  // 動きを減らす設定では回さない
  const spin = gsap.fromTo(button, { "--spin": "0deg" }, { "--spin": "360deg", duration: LAP, ease: "none", repeat: -1, paused: reduceMotion });

  // ホバーの強さ h(0〜1)から、グロー・地・矢印・回る速さをまとめて描く
  const hover = { h: 0 };
  const render = () => {
    button.style.setProperty("--glow", rest + (1 - rest) * hover.h);
    label.style.backgroundColor = gsap.utils.interpolate(bg, bgHover, hover.h);
    gsap.set(arrow, { x: NUDGE * hover.h });
    spin.timeScale(1 + (BOOST - 1) * hover.h);
  };
  // 強めるのは速く、戻すのはゆっくり。動きを減らす設定では一瞬で切り替える
  const glowUp = () => gsap.to(hover, { h: 1, duration: reduceMotion ? 0 : 0.5, ease: "power2.out", overwrite: true, onUpdate: render });
  const glowDown = () => gsap.to(hover, { h: 0, duration: reduceMotion ? 0 : 0.7, ease: "power2.inOut", overwrite: true, onUpdate: render });

  button.addEventListener("pointerenter", glowUp);
  button.addEventListener("pointerleave", glowDown);
  button.addEventListener("focus", glowUp);
  button.addEventListener("blur", glowDown);

  // 画面の外にあるあいだは止めて、見えない所で毎コマ描き直さない
  new IntersectionObserver(([entry]) => spin.paused(reduceMotion || !entry.isIntersecting)).observe(button);
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。