01Cursor spotlight
A round pool of light tracks the cursor across the button surface. Two CSS variables carry the light's centre and the gradient paints the rest.
Best forWhen several buttons sit side by side in a pricing row or top nav and a plain colour change is too weak to show which one is under the cursor
radial-gradient@propertypointermove

<main class="stage">
<button class="spot is-demo" type="button"><span class="spot__label">See pricing</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 Cursor spotlight — Measured area 7.528% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · 9 button hover effects css that award sites reuse
The post this demo lives in
9 button hover effects css that award sites reuseThese button hover effects css techniques are the reactions a button gives when a pointer arrives, sorted by what each s See all demos in this post

