CSS cursor 속성은 요소 위에 표시되는 마우스 포인터를 설정합니다. 손에는 cursor: pointer, 드래그할 수 있는 항목에는 cursor: grab, 차단된 작업에는 cursor: not-allowed를 작성하세요. 36개의 키워드 값과 자신만의 이미지를 위한 url()가 있습니다.
아래 타일을 마우스로 가리키면 각 타일을 볼 수 있습니다. 터치 스크린에는 커서가 없기 때문에 휴대폰에서는 이름만 볼 수 있습니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CSS cursor keywords</title>
<style>
body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
.note { margin: 0 0 10px; font-size: 14px; }
.touch-note { display: none; }
/* no mouse (phones, tablets): there is no cursor to show */
@media (hover: none) {
.touch-note { display: inline; }
.mouse-note { display: none; }
}
.grid {
display: grid; gap: 6px;
grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
}
.tile {
padding: 9px 4px; border-radius: 8px; text-align: center;
background: #fff; border: 1px solid #e1e4ea;
font: 13px ui-monospace, Consolas, monospace;
}
.tile:hover { border-color: #2563eb; background: #eef4ff; }
</style>
</head>
<body>
<p class="note">
<span class="mouse-note">Hover a tile to see its cursor.</span>
<span class="touch-note">A touch screen has no cursor, so here you only see the names.</span>
</p>
<div class="grid" id="grid"></div>
<script>
const names = [
'auto', 'default', 'none', 'pointer', 'text', 'vertical-text',
'help', 'context-menu', 'wait', 'progress', 'crosshair', 'cell',
'grab', 'grabbing', 'move', 'all-scroll', 'copy', 'alias',
'no-drop', 'not-allowed', 'zoom-in', 'zoom-out',
'col-resize', 'row-resize', 'ew-resize', 'ns-resize',
'nesw-resize', 'nwse-resize', 'n-resize', 'e-resize',
's-resize', 'w-resize', 'ne-resize', 'nw-resize', 'se-resize', 'sw-resize'
];
const grid = document.getElementById('grid');
for (const name of names) {
const tile = document.createElement('div');
tile.className = 'tile';
tile.textContent = name;
tile.style.cursor = name; // the whole trick: one CSS property
grid.appendChild(tile);
}
</script>
</body>
</html>
각 커서의 정확한 그림은 운영 체제에서 나오므로 wait는 한 시스템에서는 물레이고 다른 시스템에서는 모래시계입니다. 의미는 동일하게 유지됩니다.
커서 값과 각각의 사용 시기
대부분의 페이지에는 5~6개가 필요합니다. 나머지는 편집기, 테이블 및 그리기 도구용으로 존재합니다.
| 가치 | 사용자에게 알려주는 내용 | 다음에서 사용하세요 |
|---|---|---|
auto |
브라우저가 결정하게 하세요 | 기본값입니다. 텍스트는 I-빔을 얻고, 링크는 손을 잡습니다. |
default |
여기에는 특별한 것이 없습니다 | 다른 커서를 상속받은 요소 재설정 |
pointer |
클릭하면 뭔가가 이루어집니다 | 버튼, 클릭 가능한 카드, 맞춤 컨트롤 |
text |
텍스트를 선택할 수 있습니다 | 사용자 정의 편집 가능 영역 |
grab / grabbing |
이제 드래그/드래그 가능 | 슬라이더, 정렬 가능한 항목, 이동 가능한 지도 |
move |
이건 이동이 가능해요 | grab가 잘못된 느낌을 주는 Windows 또는 모양 이동 |
not-allowed |
이 작업은 차단되었습니다. | 비활성화된 컨트롤, 잠긴 영역 |
wait / progress |
바쁨 | wait는 입력을 차단하고, progress는 계속 작동할 수 있습니다. |
help |
더 많은 정보는 여기에 있습니다 | 도구 설명 또는 정의가 있는 용어 |
crosshair |
정확한 포인트 선택 | 그리기, 색상 선택, 자르기 |
zoom-in / zoom-out |
클릭하면 확대됩니다 | 이미지 썸네일 및 라이트박스 |
*-resize |
이 가장자리의 크기가 조정됩니다. | 패널 칸막이(col-resize, row-resize), 박스 코너 |
none |
커서가 전혀 없습니다 | 게임, 비디오, 사용자 정의 그려진 커서 |
크기 조정 제품군 이름 방향: ew-resize는 왼쪽 및 오른쪽, ns-resize는 위쪽 및 아래쪽, e-resize와 같은 단일 방향은 한쪽 가장자리를 가리킵니다.
한 번 설정하면 아이들이 상속받습니다.
cursor는 상속된 속성입니다. 카드에 올려놓으면 카드 안의 모든 어린이에게 추가 규칙 없이 동일한 커서가 표시됩니다.
.card { cursor: pointer; }
.card .price { cursor: text; } /* the child's own rule wins */
자신의 cursor를 설정한 자식은 부모가 말하는 대로 이를 유지합니다. 이것이 cursor: default 영역 내부의 링크가 여전히 손 모양을 표시하는 이유이기도 합니다. 브라우저의 기본 스타일은 링크에 고유한 커서 규칙을 제공합니다.
한 하위 항목에서 상속된 커서를 실행 취소하려면 브라우저의 선택을 복원하는 auto로 다시 설정하세요.
포인터는 "나를 클릭하세요"를 의미하며 다른 의미는 없습니다.
손은 사람들이 추가하는 가장 일반적인 커서이며 오용하기 가장 쉽습니다. 사용자는 이를 클릭이 작동할 것이라는 약속으로 읽습니다.

- 클릭에 반응하는 항목: 버튼, 클릭 가능한 카드, 토글 스위치, 맞춤 컨트롤에 적용하세요.
- 일반 텍스트와 라벨을 사용하지 마세요. 손 모양이 I빔을 대체하므로 텍스트가 클릭 가능한 것처럼 보이고 더 이상 선택할 수 있다는 신호가 표시되지 않습니다.
- 클릭 가능한 항목을 실제 컨트롤로 만들기:
cursor: pointer는 모양만 변경합니다. 손이 있는<div>는 여전히 키보드로 접근할 수 없습니다.<a>또는<button>를 사용하세요.
버튼에 손 모양이 표시되지만 클릭해도 아무런 반응이 없는 경우, 원인은 일반적으로 버튼 위에 다른 것이 있기 때문입니다. HTML 버튼을 클릭할 수 없음에서 해당 사례를 다룹니다.
url()을 사용한 사용자 정의 커서 이미지
자신만의 커서의 경우 이미지에 url()를 가리키고 선택적으로 핫스팟을 제공하고 키워드로 끝납니다.
.canvas {
cursor: url("pen.svg") 4 28, crosshair;
}

- 마지막 키워드는 필수입니다. 이미지가 로드되지 않을 경우 대체됩니다. 이것이 없으면 값은 유효하지 않으므로 브라우저는 이미지가 양호하더라도 전체 선언을 삭제하고 상속된 커서를 표시합니다.
- 핫스팟은 이미지의 왼쪽 상단 모서리부터 이미지 픽셀 단위로 x와 y라는 두 숫자입니다. 실제로 클릭하는 픽셀을 표시합니다. 그것이 없으면 브라우저는 파일에 저장된 핫스팟(형식에 핫스팟이 있는 경우)을 사용하거나 그렇지 않은 경우 왼쪽 상단 모서리에 저장된 핫스팟을 사용합니다.
- 형식: 브라우저는 PNG 및 SVG를 지원합니다. SVG는 루트 요소에
width및height를 설정해야 자연스러운 크기를 갖습니다. - 크기: 작게 유지하세요. MDN에서는 32 x 32 픽셀을 권장하며 Firefox와 Chromium은 기본적으로 128 x 128보다 큰 커서 이미지를 무시합니다.
여기에서 네 가지 경우를 모두 시도해 보세요. 각 상자를 클릭하면 클릭이 실제로 발생한 위치가 빨간색 점으로 표시됩니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Custom cursor image</title>
<style>
body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
.note { margin: 0 0 10px; font-size: 14px; }
.touch-note { display: none; }
@media (hover: none) { .touch-note { display: block; } }
.grid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.box {
position: relative; height: 104px; border-radius: 10px; overflow: hidden;
background: #fff; border: 1px solid #e1e4ea; user-select: none;
}
.box b { position: absolute; left: 10px; right: 10px; top: 8px; font-size: 13px; }
code { display: block; margin-top: 4px; font: 11.5px/1.4 ui-monospace, Consolas, monospace; color: #374151; }
.dot { position: absolute; width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: 50%; background: #dc2626; pointer-events: none; }
/* 32x32 SVG target. Hotspot 16 16 = its centre. Fallback: crosshair */
.ring {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Ccircle cx='16' cy='16' r='12' fill='none' stroke='%232563eb' stroke-width='3'/%3E%3Ccircle cx='16' cy='16' r='2' fill='%232563eb'/%3E%3C/svg%3E") 16 16, crosshair;
}
/* same image, no hotspot: the click lands at its top-left corner */
.ring-nohot {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Ccircle cx='16' cy='16' r='12' fill='none' stroke='%239a3412' stroke-width='3'/%3E%3Ccircle cx='16' cy='16' r='2' fill='%239a3412'/%3E%3C/svg%3E"), crosshair;
}
/* the image cannot be decoded, so the browser uses the keyword */
.broken { cursor: url("data:image/png;base64,AAAA"), pointer; }
/* no keyword at the end: the whole declaration is invalid and ignored */
.nofallback {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Ccircle cx='16' cy='16' r='12' fill='%23dc2626'/%3E%3C/svg%3E") 16 16;
}
</style>
</head>
<body>
<p class="note">Hover each box, then click. The red dot marks where the click really landed.</p>
<p class="note touch-note">On a touch screen there is no cursor. Taps still leave a dot.</p>
<div class="grid">
<div class="box ring"><b>Hotspot 16 16</b></div>
<div class="box ring-nohot"><b>No hotspot</b></div>
<div class="box broken"><b>Broken image</b></div>
<div class="box nofallback"><b>No fallback keyword</b></div>
</div>
<p class="note" id="out" style="margin-top:10px"></p>
<script>
const out = document.getElementById('out');
document.querySelectorAll('.box').forEach((box) => {
// print what the browser actually kept for this box
const kept = getComputedStyle(box).cursor;
const code = document.createElement('code');
code.textContent = kept.startsWith('url') ? 'kept: image, else ' + kept.split(',').pop().trim() : 'kept: ' + kept;
box.querySelector('b').appendChild(code);
box.addEventListener('pointerdown', (e) => {
const r = box.getBoundingClientRect();
const x = Math.round(e.clientX - r.left), y = Math.round(e.clientY - r.top);
const dot = document.createElement('span');
dot.className = 'dot';
dot.style.left = x + 'px';
dot.style.top = y + 'px';
box.appendChild(dot);
out.textContent = 'Click landed at ' + x + ', ' + y + ' inside "' + box.querySelector('b').firstChild.textContent + '".';
});
});
</script>
</body>
</html>

data: URI는 이미지를 CSS 내부에 유지하므로 페이지와 함께 이동합니다. SVG 문자열 내에서 #를 %23로, < >를 %3C %3E로 인코딩합니다.
JavaScript에서 커서 변경하기
커서 상태는 일반적으로 앱 상태(선택한 도구, 진행 중인 드래그, 잠긴 레이어)를 따릅니다. 여러 위치에 style.cursor를 작성하는 대신 클래스 또는 데이터 속성을 설정하고 CSS가 커서를 선택하도록 합니다.
.stage[data-tool="draw"] { cursor: crosshair; }
.stage[data-tool="pan"] { cursor: grab; }
.stage[data-tool="pan"].panning { cursor: grabbing; }
.stage[data-tool="draw"].locked { cursor: not-allowed; }
여기서 해당 패턴은 작은 그리기 표면을 실행합니다. 십자선으로 그리고 팬으로 전환하고 길게 눌러 grabbing를 확인한 다음 레이어를 잠그면 커서가 not-allowed로 변합니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cursor per tool</title>
<style>
body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #eceef1; color: #1d2330; }
.bar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 8px; }
button {
font: 14px system-ui, sans-serif; padding: 7px 12px; border-radius: 8px;
border: 1px solid #cfd4dc; background: #fff; color: #1d2330; cursor: pointer;
}
button[aria-pressed="true"] { background: #1d4ed8; border-color: #1d4ed8; color: #fff; }
.status { font: 13px ui-monospace, Consolas, monospace; color: #374151; }
.stage {
position: relative; height: 320px; border-radius: 12px; overflow: hidden;
background: #fff; touch-action: none; user-select: none;
}
canvas { position: absolute; left: 0; top: 0; }
/* one rule per state: the cursor says what a press will do */
.stage[data-tool="draw"] { cursor: crosshair; }
.stage[data-tool="draw"].locked { cursor: not-allowed; }
.stage[data-tool="pan"] { cursor: grab; }
.stage[data-tool="pan"].panning { cursor: grabbing; }
</style>
</head>
<body>
<div class="bar">
<button id="draw" aria-pressed="true">Draw</button>
<button id="pan" aria-pressed="false">Pan</button>
<button id="lock" aria-pressed="false">Lock layer</button>
<span class="status" id="status"></span>
</div>
<div class="stage" id="stage" data-tool="draw">
<canvas id="cv" width="1200" height="800"></canvas>
</div>
<script>
const stage = document.getElementById('stage');
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const status = document.getElementById('status');
let ox = -300, oy = -200; // how far the canvas is panned
let last = null; // last pointer position while pressed
// dotted paper, so panning is visible
ctx.fillStyle = '#d9dde3';
for (let x = 10; x < 1200; x += 20) for (let y = 10; y < 800; y += 20) ctx.fillRect(x, y, 2, 2);
ctx.lineWidth = 3; ctx.lineCap = 'round'; ctx.strokeStyle = '#1d4ed8';
function place() { cv.style.transform = `translate(${ox}px, ${oy}px)`; }
function show() { status.textContent = 'cursor: ' + getComputedStyle(stage).cursor; }
place(); show();
function setTool(tool) {
stage.dataset.tool = tool;
document.getElementById('draw').setAttribute('aria-pressed', tool === 'draw');
document.getElementById('pan').setAttribute('aria-pressed', tool === 'pan');
show();
}
document.getElementById('draw').addEventListener('click', () => setTool('draw'));
document.getElementById('pan').addEventListener('click', () => setTool('pan'));
document.getElementById('lock').addEventListener('click', (e) => {
const on = stage.classList.toggle('locked');
e.currentTarget.setAttribute('aria-pressed', on);
show();
});
stage.addEventListener('pointerdown', (e) => {
const locked = stage.dataset.tool === 'draw' && stage.classList.contains('locked');
if (locked) return; // nothing happens, as the cursor promised
stage.setPointerCapture(e.pointerId);
last = { x: e.clientX, y: e.clientY };
if (stage.dataset.tool === 'pan') { stage.classList.add('panning'); show(); }
});
stage.addEventListener('pointermove', (e) => {
if (!last) return;
if (stage.dataset.tool === 'pan') {
ox += e.clientX - last.x; oy += e.clientY - last.y; place();
} else {
const r = cv.getBoundingClientRect(); // already includes the pan offset
ctx.beginPath();
ctx.moveTo(last.x - r.left, last.y - r.top);
ctx.lineTo(e.clientX - r.left, e.clientY - r.top);
ctx.stroke();
}
last = { x: e.clientX, y: e.clientY };
});
stage.addEventListener('lostpointercapture', () => {
last = null;
stage.classList.remove('panning');
show();
});
</script>
</body>
</html>
잠긴 상태는 두 가지 작업을 수행합니다. 커서에 "차단됨"이라고 표시되고 pointerdown는 누름을 실제로 무시합니다. 코드가 다른 것을 수행하는 동안 한 가지를 말하는 커서는 커서 힌트가 없는 것보다 더 나쁩니다.
그림 자체는 HTML 캔버스 그리기에 설명되어 있고, 캔버스 확대/축소 및 이동에서 더 크게 볼 수 있습니다.
"따라가는" 커서: 커서 없음 + 요소
일부 사이트에서는 시스템 커서를 숨기고 대신 스타일이 지정된 요소를 마우스 위치로 이동합니다.
@media (hover: hover) and (pointer: fine) {
body { cursor: none; }
.dot { position: fixed; pointer-events: none; }
}
addEventListener('pointermove', (e) => {
dot.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
});
작동하며 배송하기 전에 알아야 할 비용이 있습니다.
- 미디어 쿼리에 포함시키세요. 터치 스크린에서는 탭 사이에 포인터가 움직이지 않으므로 점은 마지막 탭이 있던 곳에 위치하게 됩니다.
- 점에
pointer-events: none를 추가합니다. 그렇지 않으면 점이 포인터 아래에 위치하여 모든 클릭을 포착합니다. - 사용자 자신의 포인터를 대체합니다. 시스템 설정에서 더 크거나 고대비 포인터를 설정한 사람은 페이지에서 해당 포인터를 잃게 됩니다.
- 숨겨진 힌트도 사라집니다.
cursor: none를 사용하면 텍스트 I-빔과 링크 손을 다시 구성하지 않으면 사라집니다.
다른 모양만 필요한 경우에는 url() 커서가 더 간단하고 시스템 속도로 포인터를 계속 움직입니다.
터치스크린에는 커서가 없습니다.
터치 방식으로 사용되는 휴대폰이나 태블릿에서는 페이지 위에 아무것도 놓이지 않으므로 커서가 표시되지 않습니다. 트랙패드나 마우스가 연결된 태블릿에는 포인터가 표시될 수 있지만 포인터가 각 커서 값을 얼마나 가깝게 따르는지는 시스템에 따라 다르므로 중요한 경우 테스트해 보세요.
커서를 마우스 사용자를 위한 추가 힌트로 취급하십시오. 유일한 힌트는 아닙니다. 드래그 가능한 항목에는 핸들이나 라벨도 필요하고, 비활성화된 버튼에는 disabled 속성과 희미한 스타일이 필요하며, 클릭 가능한 카드는 핸들이나 라벨처럼 보여야 합니다.
작동하지 않을 때
| 당신이 보는 것 | 원인 | 수정 |
|---|---|---|
| 맞춤 이미지가 표시되지 않음 | 끝에 키워드가 없으므로 선언이 유효하지 않습니다. | 키워드로 끝납니다: url(...) 8 8, auto |
| 맞춤 이미지가 표시되지 않고 키워드가 있습니다. | URL이 페이지 위치에서 로드되지 않습니다. | 이미지 URL을 직접 열거나 data: URI를 사용하세요. |
| 맞춤 이미지가 키워드 커서로 표시됩니다. | 이미지가 너무 커서 브라우저가 이를 무시합니다. | 약 32x32픽셀로 크기를 조정하세요. |
| SVG 커서가 나타나지 않습니다 | SVG에는 width 및 height가 없습니다. |
<svg> 요소에 둘 다 설정 |
| 이미지에서 클릭이 발생합니다. | 핫스팟이 없으므로 왼쪽 상단 모서리가 클릭됩니다. | url() 뒤에 x y를 추가하세요(예: 16 16) |
| 자식은 부모와 다른 커서를 표시합니다. | 하위에는 자체 커서 규칙이 있거나 링크에 손을 대는 것과 같은 브라우저 기본값이 있습니다. | 자식의 커서를 명시적으로 설정 |
| 사용자가 아무 작업도 수행하지 않는 텍스트를 클릭합니다. | 클릭할 수 없는 요소에 대한 cursor: pointer |
실제 링크와 버튼에 pointer 유지 |
| 휴대폰에서는 커서가 변경되지 않습니다. | 터치스크린에는 커서가 없습니다. | 커서에 의존하지 않는 시각적 힌트 추가 |
링크로 공유하세요
커서 변경 사항은 누군가가 페이지 위로 마우스를 이동할 때만 표시되므로 스크린샷에는 표시할 수 없습니다. 대신 페이지 자체를 보내세요.
페이지를 NOS 문서에 붙여넣고 공유 링크 만들기를 선택합니다. 링크할 HTML이 이를 안내합니다.
페이지는 작성된 대로 렌더링되고 스크립트가 실행되므로 페이지를 받는 사람은 모든 타일에 마우스를 가져가서 각 도구를 직접 사용해 볼 수 있습니다. 나중에 코드를 변경하면 동일한 링크에 새 버전이 표시됩니다.