01カーソル スポットライト
カーソルの位置に合わせて丸い光がボタン表面をなぞります。光の中心座標を CSS 変数 2 つで渡すだけで、あとはグラデーションが描きます。
使いどころ料金表や上部ナビのようにボタンが並んでいて、色の変化だけでは今どれを指しているか弱いとき
radial-gradient@propertypointermove

<main class="stage">
<button class="spot is-demo" type="button"><span class="spot__label">料金プランを見る</span></button>
</main>@use "../../../../site/static/demo-tokens" as *;
// 변형은 이 세 줄만 바꾼다
$duration: $dur-loop;
$easing: $ease-out;
$color: $subject-blue;
// 커스텀 속성을 등록해야 브라우저가 퍼센트로 보간한다. 등록 안 하면 0% → 100% 로 튄다
@property --mx {
syntax: "<percentage>";
inherits: true;
initial-value: 50%;
}
@include stage($stage-ink);
.spot {
position: relative;
isolation: isolate;
overflow: hidden;
border: 0;
cursor: pointer;
padding: $sp-5 $sp-10;
border-radius: $r-pill;
background: rgba($subject-cream, .10);
box-shadow: inset 0 0 0 1px rgba($subject-cream, .30);
color: $subject-cream;
font-family: inherit;
font-size: clamp(16px, 5vw, 20px);
font-weight: 700;
}
.spot::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: radial-gradient(circle 96px at var(--mx) 50%, rgba($color, .95), rgba($color, 0) 72%);
}
.spot__label { position: relative; }
.spot.is-demo { animation: spot-sweep $duration $easing infinite; }
// 8% → 92% → 8% 로 되돌아와 끝난다. 92% 에서 끊고 처음으로 점프하면 빛이 순간이동한다
@keyframes spot-sweep {
0% { --mx: 8%; }
50% { --mx: 92%; }
100% { --mx: 8%; }
}
@media (prefers-reduced-motion: reduce) {
.spot.is-demo { animation: none; }
.spot::before { background: radial-gradient(circle 96px at 50% 50%, rgba($color, .95), rgba($color, 0) 72%); }
}@property --mx {
syntax: "<percentage>";
inherits: true;
initial-value: 50%;
}
html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.spot {
position: relative;
isolation: isolate;
overflow: hidden;
border: 0;
cursor: pointer;
padding: 20px 40px;
border-radius: 999px;
background: rgba(255, 247, 230, 0.1);
box-shadow: inset 0 0 0 1px rgba(255, 247, 230, 0.3);
color: #fff7e6;
font-family: inherit;
font-size: clamp(16px, 5vw, 20px);
font-weight: 700;
}
.spot::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: radial-gradient(circle 96px at var(--mx) 50%, rgba(47, 109, 246, 0.95), rgba(47, 109, 246, 0) 72%);
}
.spot__label {
position: relative;
}
.spot.is-demo {
animation: spot-sweep 2s ease-out infinite;
}
@keyframes spot-sweep {
0% {
--mx: 8%;
}
50% {
--mx: 92%;
}
100% {
--mx: 8%;
}
}
@media (prefers-reduced-motion: reduce) {
.spot.is-demo {
animation: none;
}
.spot::before {
background: radial-gradient(circle 96px at 50% 50%, rgba(47, 109, 246, 0.95), rgba(47, 109, 246, 0) 72%);
}
}// 실제 동작: 커서가 버튼 위에 오면 빛의 중심(--mx)이 커서를 따라가고, 손을 떼면 데모 루프로 돌아간다.
var spot = document.querySelector('.spot');
spot.addEventListener('pointermove', function (e) {
var r = spot.getBoundingClientRect();
spot.classList.remove('is-demo');
spot.style.setProperty('--mx', ((e.clientX - r.left) / r.width * 100) + '%');
});
spot.addEventListener('pointerleave', function () {
spot.style.removeProperty('--mx');
spot.classList.add('is-demo');
});import { useState } from "react";
import type { CSSProperties, PointerEvent } from "react";
import styles from "./SpotlightButton.module.scss";
export type SpotlightButtonProps = {
label?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string; "--mx"?: string };
export default function SpotlightButton({
label = "See pricing",
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: SpotlightButtonProps) {
const [mx, setMx] = useState<string | null>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
if (mx) { vars["--mx"] = mx; }
function move(e: PointerEvent<HTMLButtonElement>) {
const r = e.currentTarget.getBoundingClientRect();
setMx(`${((e.clientX - r.left) / r.width) * 100}%`);
}
return (
<button
type="button"
className={`${styles.spot} ${mx ? "" : styles.loop}`}
style={vars}
onPointerMove={move}
onPointerLeave={() => setMx(null)}
>
<span className={styles.label}>{label}</span>
</button>
);
}01 カーソル スポットライト — 計測 累積面積 7.528% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · css ボタン ホバー アニメーション 9選 — 受賞級の定番テク
このデモが属する記事
css ボタン ホバー アニメーション 9選 — 受賞級の定番テクcss ボタン ホバー アニメーションとは、ポインターが近づいたときにボタンが返す反応を、合図の種類ごとに整理したものです。9 つのうち 1 つは、押すとボタンが円になるまで縮んでスピナーが回り、チェックを描いてから元の幅へ戻ります。 この記事のデモをすべて見る

