The CSS cursor property: every value and when to use it

One CSS property sets what the mouse pointer looks like over an element. The keyword you pick is a promise about what a click or a drag will do.

The CSS cursor property sets the mouse pointer shown over an element. Write cursor: pointer for the hand, cursor: grab for something you can drag, cursor: not-allowed for an action that is blocked. There are 36 keyword values, plus url() for your own image.

Hover the tiles below with a mouse to see each one. On a phone you will only see the names, because a touch screen has no cursor.

Live exampletry it here, then copy the code
Share it as a link
<!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>
Every keyword value, one tile each. The code is a loop that sets style.cursor to the tile's name.

The exact drawing of each cursor comes from the operating system, so wait is a spinning wheel on one system and an hourglass on another. The meaning stays the same.

The cursor values and when to use each

Most pages need five or six of these. The rest exist for editors, tables and drawing tools.

Value What it tells the user Use it on
auto Let the browser decide The default. Text gets the I-beam, links get the hand
default Nothing special here Resetting an element that inherited another cursor
pointer A click does something Buttons, clickable cards, custom controls
text Text can be selected Custom editable areas
grab / grabbing Can drag / dragging now Sliders, sortable items, pannable maps
move This can be moved Windows or shapes you move, where grab feels wrong
not-allowed This action is blocked Disabled controls, locked areas
wait / progress Busy wait blocks input, progress still lets you act
help More information is here Terms with a tooltip or definition
crosshair Precise point selection Drawing, colour picking, cropping
zoom-in / zoom-out A click zooms Image thumbnails and lightboxes
*-resize This edge resizes Panel dividers (col-resize, row-resize), box corners
none No cursor at all Games, video, a custom drawn cursor

The resize family names directions: ew-resize is left and right, ns-resize is up and down, and the single-direction ones such as e-resize point at one edge.

Set it once, and children inherit it

cursor is an inherited property. Put it on a card and every child inside the card shows the same cursor, with no extra rule.

.card { cursor: pointer; }
.card .price { cursor: text; }  /* the child's own rule wins */

A child that sets its own cursor keeps it, whatever the parent says. That is also why a link inside a cursor: default area still shows the hand: the browser's default styles give links their own cursor rule.

To undo an inherited cursor on one child, set it back to auto, which restores the browser's choice.

pointer means "click me", and nothing else

The hand is the most common cursor people add, and the easiest to misuse. Users read it as a promise that a click will work.

The hand over plain text promises a click that does nothing. Keep it on what can actually be clicked.
The hand over plain text promises a click that does nothing. Keep it on what can actually be clicked.
  • Put it on things that respond to a click: buttons, clickable cards, toggle switches, custom controls.
  • Keep it off plain text and labels: the hand replaces the I-beam, so the text looks clickable and no longer signals that it can be selected.
  • Make clickable things real controls: cursor: pointer changes only the look. A <div> with the hand is still not reachable by keyboard. Use <a> or <button>.

If a button shows the hand but a click does nothing, the cause is usually something else lying over it. HTML button not clickable covers that case.

Custom cursor images with url()

For your own cursor, point url() at an image, optionally give a hotspot, and end with a keyword:

.canvas {
  cursor: url("pen.svg") 4 28, crosshair;
}
The browser tries each image in order and falls back to the keyword. Leave the keyword out and the declaration is invalid.
The browser tries each image in order and falls back to the keyword. Leave the keyword out and the declaration is invalid.
  • The keyword at the end is required. It is the fallback if no image loads. Without it the value is invalid, so the browser drops the whole declaration and shows the inherited cursor, even when the image is fine.
  • The hotspot is two numbers, x and y, in image pixels from the image's top-left corner. It marks the pixel that actually clicks. Without it the browser uses a hotspot stored in the file, if the format has one, or else the top-left corner.
  • Formats: browsers support PNG and SVG. An SVG needs width and height set on its root element so it has a natural size.
  • Size: keep it small. MDN recommends 32 by 32 pixels and notes that Firefox and Chromium ignore cursor images larger than 128 by 128 by default.

Try all four cases here. Click each box and the red dot shows where the click really landed.

Live exampletry it here, then copy the code
Share it as a link
<!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>
An inline SVG as a data: URI, so there is no file to load. Compare the hotspot, a broken image and a missing keyword.
Without a hotspot, a ring-shaped cursor clicks with its top-left corner. With 16 16 it clicks at the centre.
Without a hotspot, a ring-shaped cursor clicks with its top-left corner. With 16 16 it clicks at the centre.

A data: URI keeps the image inside the CSS, so it travels with the page. Encode # as %23 and < > as %3C %3E inside the SVG string.

Changing the cursor from JavaScript

Cursor state usually follows app state: the tool picked, a drag in progress, a locked layer. Set a class or a data attribute and let CSS pick the cursor, rather than writing style.cursor in many places:

.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; }

Here that pattern runs a small drawing surface. Draw with the crosshair, switch to Pan and hold to see grabbing, then lock the layer and the cursor turns to not-allowed.

Live exampletry it here, then copy the code
Share it as a link
<!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>
Draw, pan and lock. JavaScript only changes data-tool and two classes. CSS maps each state to a cursor.

The locked state does two things: the cursor says "blocked", and pointerdown really ignores the press. A cursor that says one thing while the code does another is worse than no cursor hint.

The drawing itself is explained in HTML canvas draw, and panning a bigger view in canvas zoom and pan.

"Follow" cursors: cursor none plus an element

Some sites hide the system cursor and move a styled element to the mouse position instead:

@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)`;
});

It works, with costs you should know before shipping it:

  • Wrap it in the media query. On a touch screen there are no pointer moves between taps, so the dot would sit wherever the last tap was.
  • Add pointer-events: none to the dot. Otherwise the dot sits under the pointer and catches every click.
  • It replaces the user's own pointer. Anyone who set a larger or high-contrast pointer in their system settings loses it on your page.
  • Hidden hints disappear too. With cursor: none, the text I-beam and the link hand are gone unless you rebuild them.

If you only need a different look, a url() cursor is simpler and keeps the pointer moving at system speed.

Touch screens have no cursor

On a phone or tablet used by touch, nothing rests over the page, so no cursor is ever shown. A tablet with a trackpad or mouse attached can show a pointer, but how closely it follows each cursor value depends on the system, so test there if it matters.

Treat the cursor as an extra hint for mouse users, never the only one. A draggable item also needs a handle or a label, a disabled button needs the disabled attribute and a dimmed style, and a clickable card needs to look like one.

When it does not work

What you see Cause Fix
Custom image never shows No keyword at the end, so the declaration is invalid End with a keyword: url(...) 8 8, auto
Custom image never shows, keyword is there The URL does not load from the page's location Open the image URL directly, or use a data: URI
Custom image shows as the keyword cursor The image is too large and the browser ignores it Resize it to about 32 by 32 pixels
SVG cursor does not appear The SVG has no width and height Set both on the <svg> element
Clicks land off from the image No hotspot, so the top-left corner clicks Add x y after url(), such as 16 16
A child shows a different cursor than the parent The child has its own cursor rule, or a browser default such as the hand on links Set the child's cursor explicitly
Users click text that does nothing cursor: pointer on non-clickable elements Keep pointer on real links and buttons
No cursor change on a phone Touch screens have no cursor Add a visible hint that does not rely on the cursor

Cursor changes only show when someone moves a mouse over the page, so a screenshot cannot show them. Send the page itself instead.

Paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can hover every tile and try each tool themselves. If you change the code later, the same link shows the new version.

Questions people ask

How do I get the hand cursor in CSS?

Use cursor: pointer. Browsers already show it over links. Add it to other elements only when clicking them does something, such as a button or a clickable card.

Why does my custom cursor image not show?

Check three things. The declaration must end with a keyword, as in url(my.png) 8 8, auto, or the browser ignores the whole line. The URL must load from the page's location. And the image must be small: large cursor images are ignored, so keep it near 32 by 32 pixels.

Can I make the cursor bigger with CSS?

Only by supplying a larger image with url(). Browsers ignore cursor images above a size limit, so there is no way to scale the system cursor itself. People who need a larger pointer usually set it in their operating system.

Does the cursor property do anything on phones?

No. A touch screen has no pointer resting on the page, so there is nothing to draw. Every cursor hint needs a second signal that also works for touch, such as a button style or a label.

Is cursor inherited?

Yes. A child shows its parent's cursor unless the child has its own cursor rule, in which case the child's rule wins. Links get their own pointer rule from the browser's default styles.

Keep reading