화면 속 요소를 손으로 집어 끌고, 옆으로 쓸어 넘기고, 손잡이를 밀어 값을 바꾸는 몸짓은 화면을 직접 만지는 감각을 줍니다. 이 직접 다루는 손맛을 CSS와 최소한의 JS로 어떻게 빚는지 코드로 보여드리겠습니다.

84.1 잡히는 손 모양 만들기

집어 들 수 있는 요소는 그렇게 생겨야 합니다. 커서를 펼친 손으로 바꾸고, 잡는 순간 쥔 손으로 넘깁니다.


.draggable {
cursor: grab;
user-select: none;
}
.draggable:active {
cursor: grabbing;
}


cursor: grab이 펼친 손을 보여 주고, 누르는 동안 :active가 쥔 손으로 바꿉니다. user-select: none은 끌다가 실수로 텍스트가 파랗게 선택돼 버리는 일을 막아, 드래그가 깔끔하게 이어지게 합니다.

84.2 어떤 제스처를 받을지 정하기

브라우저는 기본으로 스크롤과 확대를 제 몫으로 가져갑니다. 내가 직접 다룰 제스처라면 touch-action으로 브라우저의 손을 미리 떼어 놓아야 합니다.


.knob {
touch-action: none;
}
.h-scroller {
touch-action: pan-x;
}
.card {
touch-action: pan-y;
}


touch-action: none은 손잡이를 끌 때 화면이 딸려 스크롤되지 않게 브라우저의 기본 동작을 끕니다. 옆으로만 넘기는 자리엔 pan-x, 세로로만 넘기는 자리엔 pan-y를 주어, 내 제스처와 브라우저의 스크롤이 서로 부딪히지 않게 방향을 갈라 둡니다.

84.3 끌어다 놓는 자리 밝히기

집어 든 것을 놓을 수 있는 자리는 끌고 가는 동안 눈에 띄게 도드라져야 합니다. HTML의 draggable 속성과 놓는 자리의 상태 클래스로 만듭니다.


.chip[draggable="true"] {
cursor: grab;
}
.drop-zone {
border: 2px dashed #d1d5db;
border-radius: 8px;
transition: border-color .15s ease, background .15s ease;
}
.drop-zone.over {
border-color: #2563eb;
background: #eff6ff;
}


끌어 온 것이 자리 위에 들어오면 over 클래스를 붙여 테두리와 배경을 물들입니다. 클래스 토글만 JS가 맡습니다.


zone.addEventListener("dragover", (e) => {
e.preventDefault();
zone.classList.add("over");
});
zone.addEventListener("dragleave", () => {
zone.classList.remove("over");
});


dragover에서 preventDefault를 불러 줘야 그 자리가 드롭을 받아들입니다. 파란 점선으로 바뀐 자리가 여기 놓아도 된다고 사용자에게 미리 일러 주니, 놓기 전에 결과를 그려 볼 수 있습니다.

84.4 슬라이더 손잡이 다듬기

값을 밀어서 정하는 슬라이더는 기본 모양이 밋밋합니다. 브라우저 기본 옷을 벗기고 손잡이를 잡히기 좋게 다시 그립니다.


input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
border-radius: 999px;
background: #e5e7eb;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #2563eb;
cursor: grab;
}
input[type="range"]::-webkit-slider-thumb:active {
cursor: grabbing;
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
border: none;
border-radius: 50%;
background: #2563eb;
}


appearance: none으로 기본 옷을 벗긴 뒤, 크롬은 ::-webkit-slider-thumb, 파이어폭스는 ::-moz-range-thumb로 손잡이를 각각 그립니다. 손잡이를 20px 원으로 넉넉히 키우고 잡는 커서를 얹으니, 손끝이 어긋나도 편하게 밀립니다.

84.5 손끝을 따라 옮기기

요소를 끌어 옮길 땐 손끝을 지체 없이 따라와야 합니다. 성능을 위해 pointermove에서 transform만 바꿉니다.


.knob {
cursor: grab;
touch-action: none;
will-change: transform;
}


let startX = 0, x = 0;
knob.addEventListener("pointerdown", (e) => {
startX = e.clientX - x;
knob.setPointerCapture(e.pointerId);
});
knob.addEventListener("pointermove", (e) => {
if (!knob.hasPointerCapture(e.pointerId)) return;
x = e.clientX - startX;
knob.style.transform = "translateX(" + x + "px)";
});


setPointerCapture로 손가락을 이 요소에 묶어 두어, 손이 밖으로 벗어나도 계속 따라옵니다. leftmargin 대신 transform: translateX만 건드리니 레이아웃을 다시 계산하지 않고 화면 합성 단계에서 매끄럽게 미끄러집니다.

84.6 스와이프 힌트 넌지시 알리기

쓸어 넘길 수 있다는 것은 눈에 보이지 않습니다. 요소가 살짝 왔다 갔다 하며 여기 밀어 보라고 손짓하게 합니다.


@keyframes swipe-hint {
0%, 100% {
transform: translateX(0);
}
50% {
transform: translateX(-8px);
}
}
.hint {
animation: swipe-hint 1.4s ease-in-out infinite;
}


요소가 왼쪽으로 8px 슬쩍 밀렸다 돌아오기를 되풀이하며, 이쪽으로 쓸 수 있다는 실마리를 흘립니다. 보이지 않는 제스처에 이런 작은 힌트를 곁들이면, 손짓을 모르는 사용자도 길을 찾습니다.

84.7 움직임을 원하지 않는 사용자 존중하기

끊임없이 흔들리는 힌트나 부드러운 상태 전환이 불편한 사용자가 있습니다. 기능은 그대로 두되 저절로 이는 움직임만 걷어 냅니다.


@media (prefers-reduced-motion: reduce) {
.drop-zone,
input[type="range"]::-webkit-slider-thumb {
transition: none;
}
.hint {
animation: none;
}
}


스와이프 힌트의 반복 흔들림이 멈추고 상태 전환도 즉시 바뀝니다. 잡고 끌고 밀어 값을 정하는 조작 자체는 그대로 살아 있으니, 반응은 온전하되 저절로 일렁이는 움직임만 사라집니다.

84.8 정리

드래그는 cursor: grabuser-select: none으로 잡히는 손을 만들고, touch-action으로 브라우저와 제스처의 경계를 긋는 데서 시작합니다. 네이티브 드롭은 상태 클래스로 자리를 밝히고, 슬라이더는 ::-webkit-slider-thumb로 손잡이를 다듬고, 옮김은 transform: translateX만 바꿔 가볍게 하며, 보이지 않는 손짓엔 힌트를 곁들이되 움직임을 싫어하는 사용자를 늘 함께 챙깁니다.