What is the CSS cursor property?

The cursor property controls the mouse pointer’s appearance when it hovers an element. Think of it as “body language” for your UI: the cursor can hint that something is clickable, text is selectable, something can be dragged, resized, zoomed… or that you should definitely not click that.

The browser already chooses smart defaults (a text caret over text inputs, a pointer over links, etc.). Your job with CSS is mostly to:

  • Improve clarity (make custom components feel like real controls).
  • Match behavior (if it’s clickable, use pointer).
  • Avoid confusion (don’t show “text” for non-text things, don’t show “pointer” on disabled controls).
 .card { cursor: pointer; } 
 .card { cursor: text; } 
 .card { cursor: not-allowed; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .card { border: 2px solid #111; border-radius: 12px; padding: 14px; max-width: 520px; background: #f2f2f2; } .card strong { display: block; margin-bottom: 8px; } .card p { margin: 0; line-height: 1.4; } 
 
Hover me

Switch CSS snippets to see how the cursor changes.

CSS cursor syntax and fallbacks

The basic syntax is:

cursor: keyword;

Or for custom images:

cursor: url("cursor.png") x y, fallback;

  • The browser tries each value from left to right. If it can’t use the custom image, it falls back to the keyword.
  • The optional x y numbers define the “hotspot” (the exact click point) inside the image. They’re pixel coordinates from the top-left of the cursor image.
  • Always include a keyword fallback at the end. Custom cursors can fail for lots of reasons (size limits, format support, security rules).
  • Your custom cursor image should be small (typically 32x32px or less) and in a supported format (like PNG or SVG). Browsers won't support anything above 128px by 128px.
 .demo { cursor: url("https://interactivecss.com/wp-content/uploads/2026/02/StreamlineStickiesCursor.png") 32 4, pointer; } 
 .demo { cursor: pointer; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .demo { display: grid; place-items: center; width: 520px; max-width: 100%; min-height: 150px; border: 2px solid #111; border-radius: 12px; background: #fff; } .demo p { margin: 0; max-width: 44ch; text-align: center; line-height: 1.4; } 
 

Hover here. Switch snippets to compare hotspot positions and fallback behavior.

Common cursor keywords you’ll actually use

There are many cursor keywords. Most projects end up using a few of these. Here are the ones you’ll reach for constantly:

  • auto: let the browser decide (default).
  • default: the normal arrow pointer.
  • pointer: indicates something is clickable.
  • text: indicates text selection is possible.
  • move: indicates dragging/moving.
  • grab / grabbing: indicates draggable content (modern “move”).
  • not-allowed: indicates you can’t do that (great for disabled states).
  • help: indicates there’s more info (tooltips, hints).
  • zoom-in / zoom-out: indicates zoom behavior (images, maps).
  • crosshair: selection/precision (drawing tools, crop tools).
 .is-linky { cursor: pointer; }
     .is-texty { cursor: text; } 
     .is-draggable { cursor: grab; }
     .is-draggable:active {
  cursor: grabbing;
}
    .is-disabled { cursor: not-allowed; }
    .is-zoom { cursor: zoom-in; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; max-width: 680px; } .tile { border: 2px solid #111; border-radius: 12px; padding: 14px; background: #f2f2f2; } .tile strong { display: block; margin-bottom: 6px; } .tile p { margin: 0; line-height: 1.4; } .is-disabled { opacity: 0.55; } 
 
pointer

Feels clickable.

text

Feels selectable.

grab / grabbing

Feels draggable.

not-allowed

Feels disabled.

zoom-in

Feels zoomable.

auto

No cursor override here.

Targeting the right elements

A common beginner move is to slap cursor: pointer on random containers. Instead, aim the cursor at the element that actually performs the action.

  • Links and buttons usually already get the pointer cursor by default.
  • Custom “button-like” elements (divs acting like buttons) need cursor: pointer.
  • Disabled controls should feel disabled (usually not-allowed).
 .fake-button { cursor: pointer; } 
 .fake-button { cursor: default; } 
 .fake-button[aria-disabled="true"] { cursor: not-allowed; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .row { display: grid; gap: 10px; max-width: 560px; } .fake-button { display: inline-flex; align-items: center; justify-content: center; border: 2px solid #111; border-radius: 999px; padding: 10px 14px; background: #fff; user-select: none; } .fake-button[aria-disabled="true"] { opacity: 0.55; } .note { margin: 0; line-height: 1.4; } 
 
I am a div pretending to be a button
Disabled-ish button

Switch snippets. Notice how a pointer cursor makes custom controls feel like real controls.

Custom PNG cursor with hotspot

Custom cursors are just images. For a PNG cursor, you’ll usually do:

  • cursor: url("...png") x y, pointer;
  • Pick a sensible fallback keyword that matches the UI behavior (pointer, text, etc.).
  • Set a hotspot so clicking feels accurate.

Hotspot tips:

  • If your cursor image is an arrow, the hotspot should be the arrow tip.
  • If your cursor is a hand, the hotspot might be the index finger.
  • If clicks feel “offset”, your hotspot is probably wrong.
 .click-zone { cursor: url("https://interactivecss.com/wp-content/uploads/2026/02/StreamlineStickiesCursor.png") 32 2, pointer; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .click-zone { width: 560px; max-width: 100%; min-height: 180px; border: 2px dashed #111; border-radius: 12px; background: #fff; display: grid; place-items: center; text-align: center; padding: 16px; } .click-zone strong { display: block; margin-bottom: 8px; } .click-zone p { margin: 0; line-height: 1.4; max-width: 48ch; } 
 
Adjust the hotspot sliders

If the click point feels “off”, tweak X/Y until the cursor behaves like it’s touching the exact spot you intend.

Custom SVG cursor

SVG cursors can be super crisp, especially on high DPI screens. The syntax is the same: url("...svg") x y, fallback.

A couple of practical notes:

  • Some browsers are pickier with SVG cursors than PNG cursors.
  • SVG cursors can be blocked by security rules in certain contexts.
  • Always provide a keyword fallback so your UI still makes sense.
 .svg-cursor { cursor: url("https://interactivecss.com/wp-content/uploads/2026/02/BasilCursorSolid.svg") 6 6, pointer; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .svg-cursor { width: 560px; max-width: 100%; min-height: 170px; border: 2px solid #111; border-radius: 12px; background: #f2f2f2; padding: 16px; display: grid; place-items: center; text-align: center; } .svg-cursor p { margin: 0; max-width: 52ch; line-height: 1.4; } 
 

Hover here.

grab and grabbing for drag UX

If something can be dragged (cards in a kanban board, sliders, horizontal carousels), the cursor can make that obvious.

  • grab says: “you can pick this up”.
  • grabbing says: “you are currently dragging”.

CSS can’t detect “dragging” by itself. But you can often use :active as a simple approximation:

 .handle { cursor: grab; }

.handle:active {
cursor: grabbing;
}
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .handle { display: grid; place-items: center; width: 560px; max-width: 100%; min-height: 160px; border: 2px solid #111; border-radius: 12px; background: #fff; user-select: none; } .handle strong { display: block; margin-bottom: 8px; } .handle p { margin: 0; line-height: 1.4; max-width: 48ch; text-align: center; } 
 
Grab me

Press and hold the mouse button to see grabbing.

Resize cursors and where they fit

When the user can resize something, cursor choices like ew-resize and ns-resize give a clear hint. These are perfect for split panes, resizable sidebars, and draggable dividers.

  • ew-resize: left-right resizing
  • ns-resize: up-down resizing
  • nwse-resize / nesw-resize: diagonal resizing
 .divider.horizontal { cursor: ew-resize; } 
 .divider.horizontal { cursor: ns-resize; } 
 .divider.horizontal { cursor: col-resize; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .panes { display: grid; grid-template-columns: 1fr 10px 1fr; width: 680px; max-width: 100%; border: 2px solid #111; border-radius: 12px; overflow: hidden; } .pane { padding: 14px; background: #fff; } .pane:nth-child(1) { background: #f2f2f2; } .divider { background: #111; } .pane p { margin: 0; line-height: 1.4; } 
 

Pane A

Pane B

Zoom cursors for images and previews

If clicking an image zooms it (lightbox, gallery, product photos), zoom-in and zoom-out feel natural. Just remember: only use them if zoom really happens.

 .photo { cursor: zoom-in; } 
 .photo { cursor: zoom-out; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .photo { width: 560px; max-width: 100%; border: 2px solid #111; border-radius: 12px; overflow: hidden; background: #ddd; } .photo img { display: block; width: 100%; height: 240px; object-fit: cover; } .caption { padding: 12px 14px; background: #fff; } .caption p { margin: 0; line-height: 1.4; } 
 
Random placeholder image

Hover the image area. Switch snippets to compare zoom cursors.

Cursor UX rules: do and don’t

A cursor is a promise. If you show the “click me” cursor, the user expects clicking to do something. If you show “text”, they expect selection. If you show “not-allowed”, they expect a blocked action.

  • Do use pointer for genuinely clickable things.
  • Do use text only where text selection makes sense.
  • Do use not-allowed for disabled states, not “just because it looks cool”.
  • Don’t use custom cursors for everything. It gets tiring fast.
  • Don’t hide important feedback behind a cursor change. Use visible UI states too (hover styles, focus outlines, disabled styling).
 .not-good-idea { cursor: pointer; } 
 .not-good-idea { cursor: not-allowed; } 
 .good-idea { cursor: pointer; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .grid { display: grid; gap: 12px; max-width: 680px; } .box { border: 2px solid #111; border-radius: 12px; padding: 14px; background: #fff; } .box p { margin: 0; line-height: 1.4; } .not-good-idea { opacity: 0.55; } .good-idea { background: #f2f2f2; } 
 

Example: A disabled thing. Should it really look clickable?

Example: A clickable thing. Pointer fits.

Troubleshooting: why your cursor doesn’t change

If a cursor “doesn’t work”, it’s usually one of these:

  • You set it on the wrong element. The cursor uses the element currently under the pointer. If a child covers the parent, the child wins.
  • Another rule overrides yours. Check specificity, cascade order, and whether your selector actually matches.
  • Your custom cursor failed to load. Wrong URL, blocked asset, unsupported format, or the cursor is too large (above 128px * 128px).
  • No fallback provided. If the custom image fails and there’s no keyword, you’ll get confusing results.
  • No width and height set for a SVG Cursor. SVG cursors require explicit width and height attributes to display correctly.

When using custom images, keep your CSS like this:

  • cursor: url("...") x y, pointer;
  • Always end with a keyword fallback.
 .outer { cursor: pointer; }

.inner {
cursor: text;
}
 .outer { cursor: pointer; } .inner { cursor: inherit; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { padding: 18px; font-family: ui-sans-serif, system-ui, sans-serif; } .outer { border: 2px solid #111; border-radius: 12px; padding: 16px; background: #f2f2f2; max-width: 620px; } .inner { border: 2px dashed #111; border-radius: 10px; padding: 16px; background: #fff; } p { margin: 0; line-height: 1.4; } 
 

Outer has a cursor set.

Inner sits on top and can override the cursor.

Conclusion: cursor with intent

The cursor property is tiny, but it has big “UX energy”. Use it to reinforce what your UI actually does:

  • Clickable things feel clickable (pointer).
  • Text feels selectable (text).
  • Drag handles feel draggable (grab/grabbing).
  • Disabled things feel disabled (not-allowed).
  • Custom cursors are a spice, not the main course.

If you remember one rule, make it this: the cursor should never lie.