Custom Cursors in CSS: A Complete Guide

Custom cursors are one of the smallest visual details on a page, but they are also one of the easiest to get wrong. This guide covers the CSS cursor property, image formats, hotspot placement, size limits and the accessibility trade-offs of replacing the system pointer.
The cursor property and keyword values
The cursor property accepts a list of values, ending in a fallback keyword such as auto, pointer, text, move, grab or not-allowed. Browsers already ship a wide set of these keywords, and in most interfaces they communicate intent better than a custom image, because users already know what a pointer or a text caret means.
A typical declaration looks like cursor: url("cursor.png") 4 4, pointer, where the two numbers after the URL set the hotspot and pointer is the fallback if the image fails to load or is not supported. You can chain multiple url() values before the final keyword, which lets you offer more than one image format for the same cursor.
url() and hotspot coordinates
The hotspot is the exact pixel in the image that acts as the click point. Without it, browsers default to the top-left corner of the image, which is wrong for anything other than an arrow-shaped cursor. For a crosshair or a pen tip, you need to count pixels from the top-left of your source file to the point that should register the click, then supply those as the two numbers after the URL.
If you get the hotspot wrong, clicks will land in the wrong place relative to what the user sees, which is confusing on buttons, sliders and canvas-based tools. Test hotspot values directly in the browser rather than guessing, since image editors do not export this data.
Size limits and recommended dimensions
Most browsers cap custom cursor images at around 128 by 128 pixels, and some enforce this strictly by refusing to render larger files at all. In practice, 32 by 32 pixels is the safer target, matching the size of the default system cursor and staying well inside every limit.
Larger cursors also cost more to decode and repaint on every mouse move, so keeping the image small is as much a performance decision as a compatibility one. If you need a bigger visual indicator, consider an absolutely positioned element that follows the pointer instead of relying on the cursor property, but read the performance section below first.
File formats: PNG, SVG and CUR
PNG is the most reliably supported format for custom cursors across current browsers, and it handles transparency cleanly, which matters for anything that is not a solid rectangle. CUR is the native Windows cursor format and works everywhere Windows does, but it requires a separate export step most designers skip.
SVG cursors are supported in several browsers but with caveats: some browsers rasterise the SVG at a fixed size before use, ignoring the viewBox scaling you might expect, and hotspot behaviour can be inconsistent. If you use SVG, always list a PNG fallback before the final keyword so unsupported browsers still get something sensible.
Building a fallback chain
Because support varies, the safest pattern is to list your preferred format first, a broadly supported format second, and a system keyword last: cursor: url("cursor.svg"), url("cursor.png") 16 16, pointer. Browsers read the list in order and use the first one they can apply, skipping anything they do not understand or cannot load.
Always end the chain with a real keyword, not another url(). If every image in the list fails and there is no keyword, the browser falls back to auto, which may not match the interaction you intended.
Accessibility and motion concerns
Replacing the system cursor removes a visual signal that many users rely on, particularly people with low vision who have configured a larger or high-contrast system cursor. A custom cursor overrides that personal setting, so it should only be used where it adds real information, such as marking a draggable region or a custom tool mode, not purely for decoration.
Animated or trailing cursors that move independently of the actual pointer can also trigger discomfort for users sensitive to motion. Respect the prefers-reduced-motion media query and disable cursor animation or trailing effects when it is set, falling back to a static image or the system pointer.
JavaScript cursor followers and their cost
Some interfaces use a JavaScript-driven element that follows the mouse with a slight delay, rather than the native cursor property. This gives more visual freedom, such as scaling the follower on hover or blending it with content, but it runs on every mousemove event and typically triggers a style or transform update on each frame.
On lower-powered devices this adds up, especially if the follower recalculates layout instead of using a transform, or if it is combined with other animated elements on the page. If you build one, move it with transform: translate3d() rather than changing top and left, and throttle updates with requestAnimationFrame instead of running logic directly inside the event handler.
Pointer versus touch devices
The cursor property has no effect on touch input, since there is no pointer to render on a touchscreen. Any custom cursor or follower effect should be scoped with a pointer media query, using @media (pointer: fine) to target devices with an accurate pointing device such as a mouse, and left off entirely for coarse pointers.
This also protects performance on mobile, where a cursor follower would otherwise still attach event listeners and run logic for an effect nobody sees. Checking for pointer support before adding any cursor-related JavaScript avoids that wasted work.
Where to find ready-made cursors
If you want to skip building cursor images from scratch, the /ui-assets section has downloadable custom cursor sets already sized and exported for direct use with the cursor property. Pairing a downloaded cursor set with the fallback chain described above is usually faster than designing and hotspot-testing your own from zero.
A worked example: a drag handle cursor
A drag handle in a reorderable list benefits from a custom cursor that communicates the interaction more precisely than the generic move keyword. The CSS sets cursor: grab on the handle in its resting state and cursor: grabbing while the pointer is held down, which most browsers implement as built-in keywords without needing a custom image at all, making this one of the few cases where the native keyword set already covers a fairly specific interaction.
If the design calls for a custom icon instead of the built-in grab hand, export a 32 by 32 PNG with the hotspot centred on the visual grip point, reference it with cursor: url("grab.png") 16 16, grab so the keyword fallback still activates correctly if the image fails to load, and swap to a second image and hotspot pair for the grabbing state during the pointerdown to pointerup interval.
Browser support summary
The cursor property itself, and the full standard keyword set including grab, grabbing, zoom-in, zoom-out, not-allowed and the resize keywords, are supported consistently across all current browsers with no vendor prefixes required. url() cursor images with hotspot coordinates are likewise broadly supported for PNG and GIF, which have been reliable formats for this purpose for a very long time.
SVG as a cursor source is the one area with real inconsistency: some browsers rasterise it at an unexpected fixed size, and CUR file support for embedding multiple resolutions in one file works well on Windows-oriented browsers but is less consistently honoured elsewhere. Treat SVG and CUR cursors as progressive enhancements behind a PNG fallback rather than the primary format.
Performance checklist
Points to check before shipping any custom cursor or cursor-following effect.
- Keep cursor images at or under 32 by 32 pixels unless there is a specific reason to go larger
- Move JavaScript cursor followers with transform, not top and left, to avoid triggering layout
- Throttle follower position updates to animation frames rather than running on every raw mousemove event
- Scope any cursor-following code behind @media (pointer: fine) so it never runs on touch devices
- Respect prefers-reduced-motion by disabling trailing or animated cursor effects when it is set
FAQ
Why does my custom cursor not appear at all in one browser? The image most likely exceeds that browser's maximum supported cursor size, or the fallback keyword at the end of the list is missing, causing the whole declaration to be treated as invalid and ignored.
Can a custom cursor include animation, like a spinning icon? Static cursor images are the reliable, well-supported case; animated cursor formats exist historically on Windows but are not part of the CSS cursor specification, so an animated effect in a web page is almost always built with a JavaScript-driven follower element instead.
Does cursor: none have a legitimate use? Yes, for canvas-based drawing tools or games that render their own custom pointer indicator directly inside the canvas, hiding the system cursor with cursor: none avoids a confusing doubled pointer on screen.
Questions about the tools in this guide
Short answers about the hubs this article touches, each linking straight to the tool.