CSS Cursor Table

PieceWhat it doesField note
cursor: pointerThe clickable signalLinks get it free - custom clickables NEED it set explicitly
cursor: not-allowedThe blocked signalSignals disabled WITHOUT disabling clicks - pair with disabled state
cursor: grab / grabbingThe drag pairgrab on hover, grabbing while active - the drag affordance
cursor: textThe editable signalcontenteditable regions need it explicitly - divs don't get it free
cursor: wait / progressBusy signalingwait = blocked; progress = working but interactive - pick honestly
cursor: noneHidden cursorCustom cursors and kiosks - ALWAYS pair with a drawn replacement
cursor: url(...), autoCustom imagesFallback keyword REQUIRED; 32px practical size cap
cursor โ‰  disabledThe a11y lawcursor says intent; pointer-events and disabled do the blocking
Reference: the MDN cursor reference. The cursor is the cheapest affordance signal: pointer on anything clickable (custom clickables get DEFAULT cursor - links are the only free pointer), grab/grabbing pairs for draggable surfaces, not-allowed to signal blocked UI. The accessibility law to never break: CURSOR SAYS INTENT - it does not disable anything; pointer-events: none and the disabled attribute do the actual blocking, and a not-allowed cursor over a working button lies to users. Bottom line: cursor: none hides the cursor entirely - kiosks and custom-cursor games only, always with a drawn replacement. Related tools: form elements table (the disabled states cursors accompany), drag and drop table (the grab pair's job), and WAI-ARIA table (the real state signals cursors cannot carry).

The cursor is the cheapest affordance signal on the page: pointer on anything clickable (custom clickables get the DEFAULT cursor - only links are free), grab/grabbing pairs for draggable surfaces, not-allowed for blocked UI, wait and progress for busy states.

Bottom line: the accessibility law to never break - CURSOR SAYS INTENT, it does not disable anything. pointer-events: none and the disabled attribute do the actual blocking; a not-allowed cursor over a working button lies to users, and a default cursor over a clickable hides it.

The honest part: cursor: none hides the pointer entirely - kiosk interfaces, custom-cursor games and canvas drawing apps only, and always paired with a drawn replacement. A hidden cursor over normal UI is a trap, not a style.

How to use

  1. Signal every custom clickable: button, [role=button] and click-handled divs get cursor: pointer explicitly - only anchor elements inherit it for free.
  2. Pair the drag state: .handle { cursor: grab; } .handle:active { cursor: grabbing; } - the affordance changes the moment the drag begins.
  3. Be honest when busy: wait for fully blocked, progress for 'working but you can keep using other things' - the difference is respect for the user's time.

Frequently asked questions

Why doesn't my clickable div show the pointer cursor?

Because the browser only grants pointer to elements that are semantically clickable: links (a[href]) and native buttons in most engines. A div with an onclick handler is a click target to JavaScript but an ordinary text block to the cursor - the affordance gap that makes custom widgets feel dead. The fix is one line (cursor: pointer on the widget), but the deeper fix is semantic: use a real button element and the cursor, focus, keyboard and screen-reader affordances all arrive together. The cursor gap is a signal that the element is missing its semantics, not just its CSS.

What is the difference between wait and progress cursors?

Honesty about scope. wait (the hourglass or spinner cursor alone) says the entire interface is blocked - nothing responds until this finishes; use it only for full-screen blocking operations. progress (spinner PLUS arrow) says 'this thing is working; the rest of the page still works' - the correct cursor for background saves, uploads and partial loads where users can keep navigating. Choosing wait for a two-second background save locks users out of a working interface, and they will reload the page to escape. The cursor is a scope statement about what is and is not available - match it to the actual blockage.

How does the grab/grabbing pair work for draggable surfaces?

Two states of one affordance: grab (the open hand) on hover tells users this surface can be dragged; grabbing (the closed hand) while the button is down confirms the drag is live. The pair is set with cursor: grab on the element and cursor: grabbing on :active. Its importance spikes in custom drag UIs (reorderable lists, canvas editors, map panning) where no native drag affordance exists - without the pair, users have no way to know the surface responds to dragging until they try. Note the boundary with the drag-and-drop API: its dragover cursor behavior (dropEffect) applies during actual HTML drag operations; the CSS pair covers custom pointer-driven dragging that never triggers that API.

When is cursor: none acceptable?

Only when the interface draws its own pointer or needs none: canvas drawing apps with a brush preview, kiosk interfaces navigated purely by touch, games and immersive experiences with a custom cursor rendered in the scene. The rule that keeps it safe: a hidden cursor must have a VISIBLE replacement or a touch-only interaction model - a cursor: none over ordinary links and buttons is a trap where users cannot locate their own pointer. It also needs an escape: on menus or system UI, or when the pointer leaves the custom surface, the standard cursor must return. Without those two conditions, cursor: none is not a style choice, it is deleting the user's primary pointing feedback.

Related tools