GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

CSS 마우스 커서 효과 9종 — 한 줄 복붙으로

CSS 마우스 커서 효과는 브라우저 기본 화살표 대신 프로젝트에 맞춘 모양·잔상·블렌드 반응을 붙이는 것을 말합니다. 아홉 개 중 셋은 자석처럼 끌리거나 배경 색을 반전시키는, 흔한 예제 모음에서는 잘 안 보이는 조합이며, 아홉 개 전부 한 줄 복붙으로 붙습니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기순이 아니라 커스텀 커서가 실제로 쌓이는 순서로 놓았습니다. 모양 자체(01) → 좌표 추적(02~04) → 배경과의 상호작용(05~06) → 요소별 반응(07) → 기반 처리(08) → 기기 분기(09) 순입니다. 아래 코드는 효과마다 핵심 규칙만 골랐고, 좌표를 계산하는 나머지 스크립트는 zip의 vanilla/ 폴더에서 통째로 확인할 수 있습니다. 포인터 옆에 짧은 설명을 띄우는 쪽은 CSS 툴팁 말풍선 9종이고, 커서가 아니라 글자가 스스로 찍히는 효과는 CSS 타이핑 효과 9종에 있습니다.

01커스텀 커서 이미지

어떻게 움직이나

cursor 속성에 url()로 이미지나 SVG를 지정하면 그 자리의 기본 화살표가 통째로 바뀝니다.

어디에 쓰나

헤드리스 렌더는 OS 커서를 못 찍기 때문에 이 데모는 같은 물방울 모양을 DOM 요소로 재현해 움직였습니다.

커스텀 커서 이미지
새 창에서 열기

01 · 커스텀 커서 이미지 — 측정 누적면적 5.176% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

cursor: nonehotspot 좌표fallback 커서
.stage { cursor: url("cursor.svg") 12 12, auto; }  // hotspot 12,12 + auto 폴백

.cursor {                         // 데모용 재현. 실제 서비스는 cursor 속성 하나로 끝난다
  width: 26px; height: 26px;
  border-radius: 50% 50% 50% 4px; // 물방울(핀) 모양
  background: $color;
  transform: translate(var(--x, 60px), var(--y, 40px)) rotate(-45deg);
}

02링이 따라옴

어떻게 움직이나

작은 점은 포인터 좌표에 즉시 붙고, 도넛 모양 링은 같은 좌표를 transition: transform으로 따라가 살짝 뒤처집니다.

어디에 쓰나

포트폴리오 히어로 영역처럼 클릭 가능한 자리를 은근히 알리고 싶을 때 씁니다.

링이 따라옴
새 창에서 열기

02 · 링이 따라옴 — 측정 누적면적 4.766% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointermovetransform: translate지연 추적
.dot { transform: translate(var(--x), var(--y)); }        // 지연 없음

.ring {
  border: 2px solid $color;
  border-radius: 50%;
  transition: transform 180ms $easing;                     // 이 한 줄이 "뒤처짐"의 전부
  transform: translate(var(--x), var(--y));
}

03자석처럼 끌리는 버튼

버튼 마우스 오버 효과와 무엇이 다른가

버튼 중심과 포인터 사이 거리를 재서 90px 안이면 그 방향으로 거리 비례만큼 버튼을 translate합니다.

어디에 쓰나

CTA 버튼처럼 눈에 띄어야 하는 자리에 잘 맞습니다.

자석처럼 끌리는 버튼
새 창에서 열기

03 · 자석처럼 끌리는 버튼 — 측정 누적면적 4.833% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointermove거리 벡터transform: translate
stage.addEventListener('pointermove', function (e) {
  var r = stage.getBoundingClientRect();
  var x = e.clientX - r.left, y = e.clientY - r.top;
  var br = btn.getBoundingClientRect();
  var cx = br.left - r.left + br.width / 2, cy = br.top - r.top + br.height / 2;
  var dx = x - cx, dy = y - cy;
  var dist = Math.hypot(dx, dy);
  var pull = dist < 90 ? (90 - dist) / 90 : 0;      // 반경 90px 밖이면 0, 안이면 거리에 비례
  btn.style.transform = 'translate(' + (dx * pull * .35) + 'px,' + (dy * pull * .35) + 'px)';
});

04호버 시 텍스트 라벨

어떻게 움직이나

카드 위에서만 포인터를 따라다니는 말풍선에 "보기" 같은 짧은 안내를 띄웁니다.

어디에 쓰나

이미지 카드나 캐러셀에서 다음 동작을 말없이 알리는 용도입니다.

호버 시 텍스트 라벨
새 창에서 열기

04 · 호버 시 텍스트 라벨 — 측정 누적면적 3.302% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointermoveopacity 전환라벨 텍스트
.label {
  opacity: 0;
  transition: opacity 140ms $easing;
  transform: translate(var(--x), var(--y));
  pointer-events: none;                 // 카드의 실제 hover 판정을 가리지 않는다
}
.label.is-on { opacity: 1; }

05mix-blend-mode 커서

어떻게 움직이나

mix-blend-mode: difference를 준 흰 원이 포인터를 따라가며 그 아래 배경 색을 반전시킵니다.

어디에 쓰나

아홉 개 중 이 데모의 누적 면적이 20.95%로 가장 커서, 배경이 밝든 어둡든 커서가 묻히지 않는다는 걸 렌더 수치로도 확인했습니다.

mix-blend-mode 커서
새 창에서 열기

05 · mix-blend-mode 커서 — 측정 누적면적 20.953% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

mix-blend-modedifferencez-index
.cursor {
  width: 70px; height: 70px;
  border-radius: 50%;
  background: #fff;
  mix-blend-mode: difference;
  transform: translate(var(--x), var(--y));
}

06잔상 트레일

어떻게 움직이나

포인터를 따라가는 점 하나에 같은 경로를 도는 점 5~6개를 음수 animation-delay로 겹쳐, 순간마다 서로 다른 위치에 있게 해서 옅어지는 꼬리처럼 보이게 합니다.

어디에 쓰나

크리에이티브 포트폴리오에 어울립니다.

잔상 트레일
새 창에서 열기

06 · 잔상 트레일 — 측정 누적면적 2.407% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

animation-delayopacity 트랜지션잔상 점
@for $i from 1 through 6 {
  .trail span:nth-child(#{$i}) {
    opacity: 1 - $i * .13;
    animation: cursor-path $duration $easing infinite;
    animation-delay: -#{$i * 90}ms;       // 음수 delay = 이미 그만큼 재생된 상태로 시작
  }
}

07호버 시 링 확대

어떻게 움직이나

링 커서가 클릭 가능한 링크 위에 오면 커지는, 커서 쪽에서 받는 마우스 호버 효과입니다.

어디에 쓰나

판정은 :has() 하나로 처리해서 hover마다 클래스를 토글하는 JS가 따로 없습니다.

호버 시 링 확대
새 창에서 열기

07 · 호버 시 링 확대 — 측정 누적면적 3.167% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

:hover 감지transform: scale0.2초
.ring {
  border: 2px solid $color;
  border-radius: 50%;
  transition: transform 180ms $easing;
}
.stage:has(.link:hover) .ring { transform: scale(1.6); }

08기본 커서 숨김

어떻게 움직이나

cursor: none으로 브라우저 기본 화살표를 지우는, 나머지 여덟 개가 딛고 서는 처리입니다.

어디에 쓰나

이 속성 없이는 커스텀 점을 아무리 그려도 진짜 화살표와 겹쳐 두 개로 보입니다.

기본 커서 숨김
새 창에서 열기

08 · 기본 커서 숨김 — 측정 누적면적 1.843% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

cursor: none포인터 기기 감지기반 설정
.zone--custom {
  cursor: none;                     // 이 한 줄이 모든 커스텀 커서 효과의 전제
}
.cursor {
  position: absolute;
  transform: translate(var(--x), var(--y));
}

09터치 기기 대체 처리

어떻게 움직이나

@media (pointer: coarse)로 터치 기기를 감지해 커스텀 커서 스크립트를 아예 실행하지 않고, 원래 탭 동작을 그대로 남깁니다.

어디에 쓰나

모바일에서도 열리는 페이지라면 반드시 필요합니다.

터치 기기 대체 처리
새 창에서 열기

09 · 터치 기기 대체 처리 — 측정 누적면적 4.146% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointer: coarse미디어쿼리JS 건너뛰기
// pointer:fine 인 실제 기기에서만 스크립트가 돈다. 아래 미디어쿼리가 실제 폴백이다
@media (pointer: coarse) {
  .cursor { display: none; }        // 점을 지운다
  .stage { cursor: auto; }          // 기본 탭 동작을 되돌린다
}
@media (pointer: fine) {
  .stage { cursor: none; }          // 마우스가 있는 기기만 기본 화살표를 지운다
}

어디서 깨지는가 — 함정 하나

pointer-events 를 빼먹으면

아홉 개를 렌더하면서 실제로 걸린 함정은 pointer-events였습니다. 04의 말풍선과 02의 링에 pointer-events: none을 빼먹으면, 그 요소 자신이 elementFromPoint나 contains() 판정의 대상이 되어 카드나 버튼 위에 있어도 "밖에 있다"고 잘못 읽힙니다.

왜 통과시켜야 하나

커서를 그리는 요소는 늘 포인터 이벤트를 통과시켜야 그 아래 진짜 타깃이 이벤트를 받습니다. 포인터를 쫓는 반짝임을 아예 디자인 언어로 밀고 싶다면 번쩍이는 Y2K 웹 디자인의 크롬·홀로그램 부품이 그 방향의 끝입니다. 이 수정을 담아 압축한 파일을 열 때 필요한 암호는 x65p86j3이며, 아래 버튼을 누르면 내려받을 수 있습니다.

:has() 를 못 읽는 브라우저

07의 :has()는 Chrome·Edge 105+, Safari 15.4+, Firefox 121+에서 동작합니다(caniuse의 :has() 지원표). 이보다 오래된 브라우저에서는 셀렉터 자체가 무시되므로 링이 아예 안 커지는 것으로 그칩니다 — 다른 항목처럼 레이아웃이 깨지지는 않지만, 지원 브라우저를 먼저 확인하고 쓰는 게 안전합니다.

접근성

동작 줄이기에서 남긴 것

아홉 개 전부 prefers-reduced-motion: reduce에서 루프 애니메이션을 끄되, 02의 지연 추적이나 07의 확대처럼 실제 조작에 반응하는 동작은 남겼습니다. 움직임을 줄이는 설정과 기능을 아예 지우는 것은 다른 문제라서, 무엇을 끄고 무엇을 남길지는 MDN의 prefers-reduced-motion 문서를 기준으로 나눴습니다.

마우스가 없는 사람에게는

커스텀 커서 자체는 마우스로만 보이는 장식이라, 키보드나 스크린 리더로 쓰는 사람에게는 애초에 아무 의미가 없다는 점도 설계에 넣어야 합니다.

@media (prefers-reduced-motion: reduce) {
  .cursor.is-demo { animation: none; }
  .ring { transition: none; }
}

커서 효과 말고 다른 CSS 움직임은 CSS 애니메이션 카테고리 안에 모아뒀고, 사이트 운영 방식은 소개 페이지에 적어뒀습니다.

FAQ

커스텀 커서는 모바일에서도 보입니까?

아니요. 09처럼 @media (pointer: coarse)로 걸러 터치 기기에서는 스크립트를 아예 실행하지 않습니다. 터치 화면엔 애초에 상시 커서가 없으므로 대체 신호(탭 피드백 등)를 따로 준비해야 합니다. 커서가 문장을 지나가며 남기는 흔적을 글자 단위로 보여주고 싶다면 CSS 텍스트 강조 애니메이션이 그 역할을 맡습니다.

실제 프로젝트에서도 커서를 DOM 요소로 그려야 합니까?

아닙니다. 01처럼 cursor: url() 한 줄로 끝나는 경우가 많습니다. 이 글의 데모가 DOM 점을 쓴 건 헤드리스 렌더가 OS 커서를 못 찍기 때문이고, zip의 vanilla/는 실제 포인터를 그대로 추적하는 코드입니다.

마우스 커서 이펙트를 여러 개 한 페이지에 같이 써도 됩니까?

가능하지만 08의 cursor: none을 먼저 걸고, 나머지 효과가 그 위에 그리는 순서로 쌓는 걸 권합니다. 02의 링과 06의 트레일처럼 좌표를 공유하는 효과는 같은 포인터 이벤트 하나에 묶어야 어긋나지 않습니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.