07Ring grows on hover
When the ring cursor passes over an interactive element, its scale grows, visually signaling that the element can be interacted with.
Best forCard-style link lists, toolbars dense with icon buttons
:hover 감지transform: scale0.2초

<main class="stage">
<a class="link link--a" href="#">Features</a>
<a class="link link--b" href="#">Pricing</a>
<div class="cursor-pos is-demo" aria-hidden="true"><div class="ring"></div></div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 링이 두 링크를 도는 한 바퀴
$easing: $ease-spring;
$color: $subject-cream; // 링 색
@include stage($stage-orange);
.stage { position: relative; overflow: hidden; cursor: none; }
.link {
position: absolute;
padding: clamp(8px, 2.4vw, 12px) clamp(16px, 4.4vw, 22px);
border-radius: $r-pill;
background: rgba(255, 255, 255, .14);
color: $subject-ink; // AA: 주황 면(흰 14% 겹침 #d2ada4) 위 5.96:1
font-weight: 700; font-size: clamp(12px, 3.2vw, 15px); line-height: 1; font-family: inherit;
text-decoration: none;
}
.link--a { top: 22%; left: 14%; }
.link--b { bottom: 22%; right: 14%; }
.cursor-pos {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
&.is-demo { animation: cursor-path $duration $easing infinite; }
}
.ring {
width: 30px;
height: 30px;
margin: -15px 0 0 -15px;
border: 2px solid $color;
border-radius: 50%;
transition: transform 180ms $easing;
}
// 실제 조작: 링크 위에 실제 포인터가 있으면 :has() 로 링을 키운다
.stage:has(.link:hover) .ring { transform: scale(1.6); }
.cursor-pos.is-demo .ring { animation: ring-grow $duration $easing infinite; }
@keyframes cursor-path {
0% { transform: translate(90px, 60px); }
40% { transform: translate(90px, 60px); } // link--a 위
50% { transform: translate(300px, 210px); }
90% { transform: translate(300px, 210px); } // link--b 위
100% { transform: translate(90px, 60px); }
}
@keyframes ring-grow {
0%, 100% { transform: scale(1); }
20%, 40% { transform: scale(1.6); }
50%, 70% { transform: scale(1.6); }
80% { transform: scale(1); }
}
@media (pointer: coarse) {
.cursor-pos { display: none; }
.stage { cursor: auto; }
}
@media (prefers-reduced-motion: reduce) {
.cursor-pos.is-demo { animation: none; transform: translate(90px, 60px); }
.cursor-pos.is-demo .ring { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
overflow: hidden;
cursor: none;
}
.link {
position: absolute;
padding: clamp(8px, 2.4vw, 12px) clamp(16px, 4.4vw, 22px);
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
color: #17141a;
font-weight: 700;
font-size: clamp(12px, 3.2vw, 15px);
line-height: 1;
font-family: inherit;
text-decoration: none;
}
.link--a {
top: 22%;
left: 14%;
}
.link--b {
bottom: 22%;
right: 14%;
}
.cursor-pos {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
.cursor-pos.is-demo {
animation: cursor-path 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.ring {
width: 30px;
height: 30px;
margin: -15px 0 0 -15px;
border: 2px solid #fff7e6;
border-radius: 50%;
transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.stage:has(.link:hover) .ring {
transform: scale(1.6);
}
.cursor-pos.is-demo .ring {
animation: ring-grow 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes cursor-path {
0% {
transform: translate(90px, 60px);
}
40% {
transform: translate(90px, 60px);
}
50% {
transform: translate(300px, 210px);
}
90% {
transform: translate(300px, 210px);
}
100% {
transform: translate(90px, 60px);
}
}
@keyframes ring-grow {
0%, 100% {
transform: scale(1);
}
20%, 40% {
transform: scale(1.6);
}
50%, 70% {
transform: scale(1.6);
}
80% {
transform: scale(1);
}
}
@media (pointer: coarse) {
.cursor-pos {
display: none;
}
.stage {
cursor: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.cursor-pos.is-demo {
animation: none;
transform: translate(90px, 60px);
}
.cursor-pos.is-demo .ring {
animation: none;
}
}// 실제 포인터가 움직이면 링이 좌표를 따라가고, 링크 위에서는 :has() 로 링이 커진다.
if (matchMedia('(pointer: fine)').matches) {
var stage = document.querySelector('.stage'), pos = document.querySelector('.cursor-pos');
stage.addEventListener('pointermove', function (e) {
pos.classList.remove('is-demo');
var r = stage.getBoundingClientRect();
pos.style.transform = 'translate(' + (e.clientX - r.left) + 'px,' + (e.clientY - r.top) + 'px)';
});
}import { useState } from "react";
import type { CSSProperties, PointerEvent } from "react";
import styles from "./HoverGrowRing.module.scss";
export type HoverGrowRingProps = {
links?: string[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function HoverGrowRing({
links = ["기능", "가격"],
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#fff7e6",
}: HoverGrowRingProps) {
const [demo, setDemo] = useState(true);
const [pos, setPos] = useState({ x: 90, y: 60 });
const [grow, setGrow] = useState(false);
const move = (e: PointerEvent<HTMLDivElement>) => {
setDemo(false);
const r = e.currentTarget.getBoundingClientRect();
setPos({ x: e.clientX - r.left, y: e.clientY - r.top });
setGrow((e.target as HTMLElement).closest("a") !== null);
};
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.stage} style={vars} onPointerMove={move}>
<a className={`${styles.link} ${styles.linkA}`} href="#">{links[0]}</a>
<a className={`${styles.link} ${styles.linkB}`} href="#">{links[1]}</a>
<div
className={demo ? `${styles.cursorPos} ${styles.isDemo}` : styles.cursorPos}
style={demo ? undefined : { transform: `translate(${pos.x}px,${pos.y}px)` }}
aria-hidden="true"
>
<div className={grow ? `${styles.ring} ${styles.ringGrow}` : styles.ring} />
</div>
</div>
);
}07 Ring grows on hover — Measured area 3.167% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · 9 Custom Cursor CSS Effects — One Line, Copy-Paste
The post this demo lives in
9 Custom Cursor CSS Effects — One Line, Copy-PasteA custom cursor css effect swaps the browser's plain arrow for a shape, ring, or trailing dot built from CSS transforms See all demos in this post


