마우스 이펙트 9가지 — 커서부터 드래그까지
마우스 이펙트는 커서를 움직이거나 클릭·드래그·스크롤할 때 화면이 그에 맞춰 반응하는 작은 인터랙션을 말한다. 커서만 움직였을 뿐인데 버튼이 끌려오거나 카드가 기울어지는 것도 여기 속한다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 커서 모양 변신
- 02 마그네틱 풀 버튼
- 03 스포트라이트 카드 리빌
- 04 3D 틸트 카드
- 05 드래그 스크롤 갤러리
- 06 커스텀 우클릭 메뉴
- 07 텍스트 선택 형광펜 색
- 08 스크롤 진행률 커서 링
- 09 호버 캡션 이미지 그리드
아래 아홉 개는 인기순이 아니라 마우스가 화면 위에서 실제로 겪는 순서다 — 가만히 있다가 링크 위에서 모양이 바뀌고(01), 버튼에 다가가면 끌려오고(02), 카드 위를 지나가면 빛이 따라오고 기울어지고(03·04), 화면을 손으로 끌어당기고(05), 우클릭하면 메뉴가 뜨고(06), 문장을 그으면 색이 칠해지고(07), 페이지를 읽어 내리면 링이 채워지고(08), 마지막으로 그리드를 훑으면 캡션이 올라온다(09).
01커서 모양 변신
링크 위로 커서가 올라가면 작은 점이 화살표가 든 파란 원으로 커지고, 벗어나면 다시 점으로 돌아간다. pointermove로 커서 점의 위치를 옮기고, pointerenter·pointerleave로 모양을 바꾼다.
area.addEventListener('pointermove', function (e) {
var r = area.getBoundingClientRect();
cursor.style.transform =
'translate(' + (e.clientX - r.left) + 'px,' +
(e.clientY - r.top) + 'px)';
});
link.addEventListener('pointerenter', function () {
cursor.classList.add('is-link');
});
link.addEventListener('pointerleave', function () {
cursor.classList.remove('is-link');
});
02마그네틱 풀 버튼
버튼 근처로 커서를 가져가면 버튼이 그 방향으로 살짝 끌려오고, 커서가 멀어지면 원래 자리로 돌아온다. 히트 존 중심과 커서의 거리에 계수(0.4)를 곱해 translate 값을 만든다.
zone.addEventListener('pointermove', function (e) {
var r = zone.getBoundingClientRect();
var dx = e.clientX - (r.left + r.width / 2);
var dy = e.clientY - (r.top + r.height / 2);
btn.style.transform =
'translate(' + dx * 0.4 + 'px,' + dy * 0.4 + 'px)';
});
zone.addEventListener('pointerleave', function () {
btn.style.transform = '';
});
03스포트라이트 카드 리빌
카드 위에서 커서를 움직이면 동그란 빛이 함께 움직이며, 빛이 지나간 자리의 흐린 글자가 밝게 드러난다. mix-blend-mode: soft-light로 빛과 배경을 섞고 screen으로 글자를 밝힌다.
card.addEventListener('pointermove', function (e) {
var r = card.getBoundingClientRect();
spot.style.transform =
'translate(' + (e.clientX - r.left) + 'px,' +
(e.clientY - r.top) + 'px)';
});
card.addEventListener('pointerleave', function () {
spot.style.transform = '';
});
043D 틸트 카드
카드 위 커서의 위치에 따라 카드가 입체적으로 기울고, 대각선 광택이 함께 스친다. 커서의 좌표를 0~1 비율로 바꿔 rotateX·rotateY 각도를 계산한다.
wrap.addEventListener('pointermove', function (e) {
var r = wrap.getBoundingClientRect();
var px = (e.clientX - r.left) / r.width;
var py = (e.clientY - r.top) / r.height;
var rx = (0.5 - py) * 6 * 2;
var ry = (px - 0.5) * 6 * 2;
card.style.transform =
'perspective(600px) rotateX(' + rx + 'deg) rotateY(' + ry + 'deg)';
});
05드래그 스크롤 갤러리
타일을 손으로 잡아끌듯 드래그하면 갤러리가 가로로 스크롤되고, 손을 놓으면 그 자리에 멈춘다. transform이 아니라 진짜 scrollLeft를 옮기므로 터치에서도 네이티브 스크롤과 자연스럽게 이어진다.
vp.addEventListener('pointerdown', function (e) {
dragging = true;
startX = e.clientX;
startScroll = vp.scrollLeft;
vp.setPointerCapture(e.pointerId);
});
vp.addEventListener('pointermove', function (e) {
if (!dragging) return;
vp.scrollLeft = startScroll - (e.clientX - startX);
});
06커스텀 우클릭 메뉴
영역을 우클릭하면 브라우저 기본 메뉴 대신 복사·수정·삭제 아이콘 메뉴가 클릭한 자리에 튀어 오르듯 뜬다. 좌표는 영역 경계를 넘지 않도록 클램프한다.
pad.addEventListener('contextmenu', function (e) {
e.preventDefault();
var r = pad.getBoundingClientRect();
var x = Math.min(e.clientX - r.left, r.width - 132);
var y = Math.min(e.clientY - r.top, r.height - 112);
menu.style.left = x + 'px';
menu.style.top = y + 'px';
pad.classList.add('is-open');
});
07텍스트 선택 형광펜 색
문장을 드래그로 선택하면 브라우저 파란색 대신 지정한 형광펜 색으로 칠해지고, 선택 위로 복사 배지가 뜬다. 색은 ::selection 하나로 정하고, 배지 위치는 selectionchange로 잡는다.
::selection {
background: #ff9ebb;
color: #17141a;
}
document.addEventListener('selectionchange', function () {
var sel = window.getSelection();
if (sel.isCollapsed) return;
var r = sel.getRangeAt(0).getBoundingClientRect();
badge.style.left = r.left + r.width / 2 + 'px';
badge.style.top = r.top - 28 + 'px';
badge.classList.add('is-shown');
});
08스크롤 진행률 커서 링
안에서 스크롤하는 동안 커서를 따라다니는 작은 링이 진행률만큼 채워지고, 끝까지 읽으면 체크 표시로 바뀐다. 링의 stroke-dashoffset을 스크롤 비율만큼 줄인다.
box.addEventListener('scroll', function () {
var max = box.scrollHeight - box.clientHeight;
var pct = max > 0 ? box.scrollTop / max : 0;
ring.style.setProperty('--dash', (94.2 * (1 - pct)).toFixed(1));
box.classList.toggle('is-done', pct > 0.98);
});
09호버 캡션 이미지 그리드
그리드 타일에 마우스를 올리면 아래에서 흰 캡션 띠가 올라오며 이름표가 나타나고, 키보드 포커스에도 똑같이 반응한다. JS 없이 :hover와 :focus-visible 두 선택자만으로 동작한다.
.hg-tile__cap {
transform: translateY(100%);
transition: transform .3s ease-out;
}
.hg-tile:hover .hg-tile__cap,
.hg-tile:focus-visible .hg-tile__cap {
transform: translateY(0);
}
마우스 이펙트, 어디서 깨지는가
가장 먼저 겪은 함정은 04번 3D 틸트였다. rotateX·rotateY는 시각적으로만 기울어 보일 뿐 레이아웃을 다시 계산하지 않는데도, 320px 폭에서는 회전한 카드의 눈에 보이는 경계 상자가 실제 스크롤 영역에 그대로 잡혀 화면이 옆으로 넘쳤다 — 처음 잡았던 9도로는 넘침이 그대로 남았고, 각도를 6도로 줄이고 카드 자체를 더 작게 잡은 뒤에야 320px 렌더에서 넘침이 사라진 것을 확인했다. 두 번째는 05번 드래그 갤러리다. 손가락으로 드래그하는 동작을 transform으로 흉내 내면 실제 스크롤 위치와 어긋나므로, 진짜 사용자 조작에는 반드시 scrollLeft를 직접 옮겨야 한다. 세 번째는 06번 우클릭 메뉴다 — 좌표를 클램프하지 않으면 화면 오른쪽·아래 끝에서 메뉴가 영역 밖으로 잘려 나간다.
접근성 — 커스텀 커서가 오히려 방해가 될 때
01·03·08번처럼 커서 자체를 따라다니는 요소는 prefers-reduced-motion: reduce에서 움직임을 멈추도록 transition·animation을 전부 꺼둔다. 하지만 진짜 문제는 움직임보다 커서 자체다 — 저시력 사용자나 트랙패드 대신 스위치·조이스틱 같은 보조기기를 쓰는 사람에게 작은 커스텀 커서는 기본 커서보다 오히려 찾기 어렵다. 그래서 01번은 @media (hover: none), (pointer: coarse)로 터치 기기에서 커서 점 자체를 완전히 없애고, 08번은 같은 조건에서 기본 커서로 되돌린다. 02·04번은 애초에 따로 숨길 커서 요소가 없어 포인터에 의존하는 전환만 꺼진다. 03번은 스포트라이트를 끄는 것과 함께 글자 색을 처음부터 밝혀 내용이 항상 읽히게 했다. 05번 드래그 갤러리는 오히려 터치에서 더 잘 동작한다 — 네이티브 스크롤이 그대로 살아 있기 때문이다. 06·07·09번은 마우스 조작(우클릭·드래그 선택·호버)에 각각 키보드·포커스 대체 수단(Escape·키보드 선택·:focus-visible)을 붙여, 마우스가 없어도 같은 기능을 쓸 수 있게 했다. 아홉 개 파일의 압축 비밀번호는 c5gmgmdf이고, 압축을 풀면 vanilla·react 폴더에서 같은 아홉 가지 동작을 나란히 비교해 볼 수 있다.
FAQ
마우스 이펙트는 CSS만으로 만들 수 있나요?
일부는 그렇다. 07번 텍스트 선택 색과 09번 호버 캡션 그리드는 ::selection과 :hover만으로 동작하는 CSS 전용 효과다. 나머지는 커서 좌표를 읽어야 해서 pointermove 같은 짧은 바닐라 JS가 필요하다.
커스텀 커서를 쓰면 사이트가 느려지나요?
pointermove마다 transform만 바꾸면 레이아웃 재계산이 없어 대부분 부드럽다. 다만 이벤트 안에서 매번 getBoundingClientRect를 다시 재는 대신, 크기가 바뀌지 않는 요소라면 값을 한 번 캐싱해 두는 것이 좋다.
이 효과들은 모바일에서도 그대로 동작하나요?
포인터를 전제로 한 01·02·03·04·08번은 손가락엔 의미가 없어 CSS로 숨기거나 대체 상태를 준다. 05번 드래그 갤러리와 06·07·09번은 터치에서도 자연스럽게 동작하도록 따로 처리했다.
출처: MDN Pointer events, MDN prefers-reduced-motion · 더 보기: 소개, 호버 인터랙션 모음, 랜딩 페이지 인터랙션