관리 메뉴

웹솔루션개발 26년 노하우! 해피CGI의 모든것

[해피CGI][cgimall] :has()로 만드는 커스텀 커서 인터랙션 데모 본문

웹프로그램밍 자료실/기타 자료

[해피CGI][cgimall] :has()로 만드는 커스텀 커서 인터랙션 데모

해피CGI윤실장 2026. 2. 9. 09:20

화면에 **커서 전용 DOM(.cursor)**을 띄워두고, 마우스 위치를 CSS 변수 --mx, --my로 전달해 커서가 따라오도록 구성한 예제입니다.

링크/버튼에 마우스를 올리면 body:has(...) 선택자로 상태를 감지해 커서 크기·표시 텍스트(예: “View”)·아이콘 표시가 자동 전환됩니다.

사용 방법은 간단합니다. 페이지에 .cursor 마크업을 포함하고, 커서 상태를 바꾸고 싶은 요소에 cursor-read, cursor-icon 같은 클래스를 붙이면 됩니다.

또한 JS는 mousemove 이벤트로 좌표를 받고, 약간의 지연(부드러운 추적)을 적용해 --mx/--my를 갱신합니다.

참고로 :has()는 브라우저 지원 범위가 있으니(구형 환경) 운영 적용 전 호환성 체크를 권장합니다.

HTML 구조

 

<div class="demo">

  <div class="demo__elements">

    <a href="#!" class="demo__button">Default</a>

 

    <a href="#!" class="demo__button cursor-read">Word</a>

 

    <button href="#!" class="demo__button cursor-icon">Icon</button>

  </div>

</div>

 

<div class="cursor">

  <div class="cursor__pointer cursor__pointer--default"></div>

  <div class="cursor__pointer cursor__pointer--action cursor__pointer--read">

    View

  </div>

  <div class="cursor__pointer cursor__pointer--action cursor__pointer--icon">

    <svg

      class="icon icon-plus cursor__icon"

      xmlns="http://www.w3.org/2000/svg"

      xmlns:xlink="http://www.w3.org/1999/xlink"

      width="20"

      height="20"

      viewBox="0 0 20 20"

    >

      <g id="Group_1" data-name="Group 1" transform="translate(-0.75 -0.75)">

        <line

          id="Line_1"

          data-name="Line 1"

          class="icon-plus-line"

          y2="10"

          transform="translate(10.75 5.75)"></line>

        <line

          id="Line_2"

          data-name="Line 2"

          class="icon-plus-line"

          y2="10"

          transform="translate(15.75 10.75) rotate(90)"></line>

      </g>

    </svg>

  </div>

</div>



CSS 소스

body {

  padding: 1rem;

  font-family: sans-serif;

  display: grid;

  place-items: center;

  min-height: 100vh;

}

 

a,

button {

  display: inline-block;

  border: 1px solid gray;

  padding: .5rem 1rem;

  border-radius: 4px;

  text-decoration: none;

    margin: 0;

    width: auto;

    overflow: visible;

    background: transparent;

    color: inherit;

    font: inherit;

    line-height: inherit;

    -webkit-font-smoothing: inherit;

    -moz-osx-font-smoothing: inherit;

    -webkit-appearance: none;

}

 

.demo__elements {

  display: flex;

  gap: 1rem

}

 

.cursor {

    --cursor-diameter: 75px;

    margin-top: 0;

    position: fixed;

    top: 0;

    left: 0;

    translate: calc(var(--mx) - var(--cursor-diameter) / 2)

      calc(var(--my) - var(--cursor-diameter) / 2);

    width: var(--cursor-diameter);

    aspect-ratio: 1/1;

    pointer-events: none;

    display: grid;

    z-index: 1000;

    opacity: 0;

    scale: 0;

    transition: opacity 0.25s ease;

  }

 

  :root:hover .cursor {

    opacity: 1;

    scale: 1;

  }

 

  .cursor__pointer {

    grid-row: 1;

    grid-column: 1;

    position: relative;

    width: var(--cursor-diameter);

    height: var(--cursor-diameter);

    transform-origin: 50% 50%;

    border-radius: 50%;

    background: hotpink;

    transition: 0.25s ease;

    scale: 0.2;

    /* opacity: 0.5; */

    color: white;

  }

 

  .cursor__pointer--action {

    opacity: 0;

    display: flex;

    align-items: center;

    justify-content: center;

    font-size: 14px;

    font-family: monospace;

    text-transform: uppercase;

  }

 

.cursor__pointer--icon {

  stroke: currentColor;

}

 

  .cursor__icon {

    scale: 2;

  }

 

  .cursor__pointer--default {

    opacity: 0.5;

  }

 

  body:has(:is(a:hover, button:hover)) .cursor__pointer {

    scale: 0.5;

  }

 

  body:has(:is(a:hover, button:hover)) .cursor__pointer--default {

    opacity: 0.5;

  }

 

  body:has(.cursor-read:hover) .cursor__pointer {

    scale: 1;

  }

 

  body:has(.cursor-read:active) .cursor__pointer {

    scale: 0.9;

  }

 

  body:has(.cursor-read:hover) .cursor__pointer--read {

    opacity: 1;

  }

 

  body:has(.cursor-icon:hover) .cursor__pointer {

    scale: 0.5;

  }

 

  body:has(.cursor-icon:active) .cursor__pointer {

    scale: 0.4;

  }

 

  body:has(.cursor-icon:hover) .cursor__pointer--icon {

    opacity: 1;

  }

 

  body:has(:is(a:active, button:active)) .cursor__pointer--default {

    scale: 0.4;

  }



JS 소스

const delay = 5;

 

  let posX = 0,

    posY = 0,

    mouseX = 0,

    mouseY = 0;

 

  const mouseDelay = () => {

    posX += (mouseX - posX) / delay;

    posY += (mouseY - posY) / delay;

 

    document.documentElement.style.setProperty("--mx", posX + "px");

    document.documentElement.style.setProperty("--my", posY + "px");

 

    requestAnimationFrame(mouseDelay);

  };

 

  mouseDelay();

 

  document.addEventListener("mousemove", (e) => {

    mouseX = e.clientX;

    mouseY = e.clientY;

  });


 

 

 

Comments