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 meSwitch 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 ynumbers 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; }
pointerFeels clickable.
textFeels selectable.
grab / grabbingFeels draggable.
not-allowedFeels disabled.
zoom-inFeels zoomable.
autoNo 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; }
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 slidersIf 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.
grabsays: “you can pick this up”.grabbingsays: “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 mePress 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 resizingns-resize: up-down resizingnwse-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; }
![]()
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
pointerfor genuinely clickable things. - Do use
textonly where text selection makes sense. - Do use
not-allowedfor 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.
