Front-end developers and designers writing CSS

How to change the cursor in CSS, with examples

One property controls the mouse cursor in CSS. This guide goes from a single keyword to a custom image with a hotspot and fallbacks, then covers the cases that trip people up.

Updated

Step 1: Set a keyword on the element

.card {
  cursor: pointer;
}

That is the whole job for most cases. A few things to know straight away:

  • You do not need :hover. The cursor only ever shows while the pointer is over the element, so .card:hover { cursor: pointer; } does nothing extra.
  • cursor is inherited. Everything inside .card gets the hand too, including its text. Reset children that should behave differently, for example .card p { cursor: auto; }.
  • The initial value is auto. The browser then picks the I-beam over text, the hand over links and the arrow elsewhere.

Step 2: Pick the right keyword

There are 36 keywords. These cover most interfaces:

You want to sayKeyword
This is clickablepointer
This is disablednot-allowed
You can drag thisgrab, then grabbing while dragging
This window or object movesmove
Drag this border to resize a columncol-resize
Drag this edge or cornerew-resize, ns-resize, nwse-resize, nesw-resize
Click to enlargezoom-in
Aim preciselycrosshair
Hover for an explanationhelp
Working, but you can carry onprogress
Busy, please waitwait

The CSS cursor reference has a page for every keyword with a live stage, so you can see what your own system draws.

Step 3: Use your own image with url()

.brand-zone {
  cursor: url("cursor.png") 4 2, auto;
}
  • The keyword at the end is required. Without it the whole declaration is invalid and ignored.
  • The two numbers are the hotspot: the pixel, counted from the image’s top-left corner, that does the clicking. For an arrow it is the tip. Leave them out and the browser uses the hotspot stored in a .cur file, or 0 0.
  • Keep the image at 32x32 pixels. Anything above 128x128 is ignored, and larger cursors get swapped for the fallback near the edges of the page. The size limits guide has the details.
  • SVG works in current browsers and stays sharp on high-density screens. Give the root svg element width and height attributes.

The cursor maker exports a PNG and this CSS line together, with the hotspot already filled in.

Step 4: Add fallbacks in a sensible order

.brand-zone {
  cursor:
    url("cursor.svg") 4 2,
    url("cursor.png") 4 2,
    auto;
}

The browser tries each image in order and uses the first one that loads and fits the rules. The keyword is the last resort. Pick a keyword that means the same thing as your image: pointer behind a custom hand, auto or default behind a custom arrow.

Step 5: Handle states and drags

button:disabled,
[aria-disabled="true"] {
  cursor: not-allowed;
}

.card {
  cursor: grab;
}
.card:active {
  cursor: grabbing;
}

/* While a drag is running, keep the closed hand even off the card */
html.is-dragging,
html.is-dragging * {
  cursor: grabbing;
}

Add is-dragging to the html element on pointerdown and remove it on pointerup and pointercancel. Without it, the cursor flips back the moment the pointer runs ahead of the element.

One exception: during native HTML drag and drop (the draggable attribute and dragstart), the operating system draws the drag cursor and ignores your CSS. Set event.dataTransfer.dropEffect to copy, move, link or none in dragover instead.

Step 6: Change the cursor for the whole page

Put the rule on the root element, not on body:

html {
  cursor: url("cursor.png") 4 2, auto;
}

When the pointer is below a short page’s content, there is no body underneath it, and the browser uses the root element’s cursor. Remember that links, buttons and fields set their own cursors, so they will not show your image unless you target them too.

Step 7: Change the cursor from JavaScript

Toggle a class rather than writing styles directly. It keeps the cursors in your CSS and is easy to undo:

document.documentElement.classList.add('is-busy');
try {
  await saveDraft();
} finally {
  document.documentElement.classList.remove('is-busy');
}
html.is-busy,
html.is-busy * {
  cursor: progress;
}

The finally block matters: without it, an error leaves the busy cursor on screen for good.

Step 8: Check why a cursor is not showing

  1. Is the rule crossed out in developer tools? A url() without a closing keyword is invalid.
  2. Is another rule winning? A more specific selector, or a framework’s reset, may set the cursor on the same element.
  3. Does the element have pointer-events: none? Then the pointer goes straight through it and you see the cursor of whatever is underneath.
  4. Is the image too big, or near an edge? Above 128x128 it is ignored. Near the page edges, browsers swap larger cursors (and, in Chrome 156 and Safari, any cursor) for the fallback.
  5. Is the pointer over a scrollbar or a native control? Browsers may keep their own cursor there.
  6. Is it inside an iframe? The page inside the frame decides the cursor there, not yours.

FAQ

Do buttons need cursor: pointer?

Browsers give links the hand and buttons the arrow. Most websites add pointer to buttons so everything clickable looks the same, and that is fine. Just remove it from disabled buttons.

How do I hide the cursor with CSS?

cursor: none. Use it only where something else shows the pointer’s position, keep it out of form fields, and scope it to a section. The hide-native-cursor pattern does all three.

Can a CSS cursor be animated?

No. Browsers show a single frame of an animated GIF and do not accept .ani files. For an animated cursor on a website, use a JavaScript follower such as the dot and ring follower.