Skip to content

Custom Cursors in CSS: The Property, the JS Follower, and When to Skip Both

Key Takeaways

  • A custom cursor needs three parts: the image URL, two hotspot numbers, and a fallback keyword. The keyword is mandatory, and omitting it makes the whole declaration invalid so it is discarded.
  • Firefox and Chromium cap cursor images at 128 by 128 pixels by default and MDN recommends 32 by 32. Anything above the cap is ignored silently rather than scaled down.
  • The two hotspot numbers set the exact click point inside the image. Omit them and the point defaults to the top-left corner, so a centred crosshair in a 32 by 32 image clicks 16 pixels off target.
  • The large follower cursors on agency sites are a different technique: hide the real cursor and move a positioned element with transform inside requestAnimationFrame, using pointer-events: none so it never swallows clicks.
  • Gate the effect behind @media (pointer: fine) and restore the system cursor under prefers-reduced-motion. On iPadOS only the text value changes the pointer, so custom cursor images never apply there.

The first custom cursor I ever shipped worked perfectly on my machine and did nothing at all on the client’s. No error, no warning, no fallback. The pointer just stayed an arrow. The file was a 200 by 200 pixel PNG, which my browser happened to tolerate and theirs quietly refused. That is the thing about this property: when it fails, it fails without telling anyone, and you only find out because someone mentions the cursor looks normal.

Table Of Contents

The Property Itself

A custom cursor is one CSS declaration. The complexity is entirely in the rules around it.

.custom-area {
  cursor: url("cursor.png") 4 12, auto;
}

Three parts. The image, the hotspot coordinates, and the fallback keyword. Miss any one and you get behaviour ranging from slightly wrong to nothing at all.

The Fallback Keyword Is Not Optional

This is the single most common reason a custom cursor does nothing. The MDN cursor reference is explicit: a keyword value must be specified, indicating either the type of cursor to use, or the fallback cursor to use if all specified icons fail to load.

So this is invalid and will be dropped entirely:

/* Invalid. No fallback keyword. The whole declaration is discarded. */
cursor: url("cursor.png");

/* Valid. */
cursor: url("cursor.png"), auto;

/* Valid, with several candidates tried in order. */
cursor: url("cursor.svg") 8 8, url("cursor.png") 8 8, pointer;

Pick the fallback to match the meaning of the element rather than defaulting to auto everywhere. On a clickable card, pointer is the honest fallback, because if your image fails the user still gets the signal that the thing is clickable. Falling back to auto there loses that.

Size Limits: Why Your Cursor Is Being Ignored

This is what bit me on that first project. MDN notes that while the specification does not limit the cursor image size, user agents commonly restrict them to avoid potential misuse. Specifically, in Firefox and Chromium, cursor images are restricted to 128 by 128 pixels by default, and the documentation recommends limiting the cursor image size to 32 by 32 pixels.

The failure mode is the important part: cursor changes using images larger than the user agent maximum supported size will generally just be ignored. Not scaled down. Not warned about. Ignored.

So the practical rule is 32 by 32 for anything you actually ship, and treat 128 by 128 as the hard ceiling rather than a target. If your design calls for a large circular cursor, the CSS property is the wrong tool and you want the JavaScript approach further down.

SizeResult
32 by 32Recommended by MDN. Safe everywhere.
Up to 128 by 128Default ceiling in Firefox and Chromium.
Larger than the ceilingDeclaration ignored. No fallback image is tried.
Cursor image size behaviour per the MDN cursor documentation. Oversized images fail silently rather than scaling.

Hotspot Coordinates, the Part Everyone Skips

The two numbers after the URL are the hotspot: the exact pixel inside your image that counts as the pointer tip. MDN describes them as relative to the top left corner of the image, which corresponds to 0 0, and clamped within the boundaries of the cursor image. If the values are not specified, they may be read from the file itself, and will otherwise default to the top-left corner.

That default is why so many custom cursors feel subtly broken. If you draw a crosshair centred in a 32 by 32 image and omit the hotspot, the actual click point is the top left corner, 16 pixels up and 16 pixels left of where the crosshair appears to be pointing. Everything the user clicks lands slightly off from where they aimed.

/* Crosshair: hotspot at the centre of a 32x32 image */
.canvas { cursor: url("crosshair.png") 16 16, crosshair; }

/* Arrow: hotspot at the tip, near the top left */
.stage  { cursor: url("arrow.png") 2 2, default; }

/* Magnifier: hotspot at the centre of the lens, not the handle */
.zoom   { cursor: url("magnify.png") 12 12, zoom-in; }

Set the hotspot deliberately every time, even when the answer is 0 0. It costs two characters and removes a whole class of complaints that get reported as the site feeling imprecise.

Which Image Formats Actually Work

Per MDN, user agents are required by the specification to support PNG files, SVG version 1.1 files in secure static mode that contain a natural size, and any other non-animated image file formats that they support for images in other properties. Desktop browsers also broadly support the .cur format.

Two details matter in practice. The SVG must contain a natural size, meaning explicit width and height attributes, not just a viewBox. An SVG sized only by viewBox is a common silent failure. And animated cursors are optional rather than required: MDN says user agents should also support SVG in secure animated mode, which means you cannot rely on it.

Ship PNG for anything that has to work. Use SVG when you control the audience and you have set an explicit size.

The JavaScript Follower Cursor

Everything above describes replacing the pointer bitmap. The large circles and blend-mode blobs on agency sites are a different technique: the real cursor is hidden, and a positioned element follows the mouse.

.cursor-dot {
  position: fixed;
  top: 0;
  left: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 2px solid currentColor;
  pointer-events: none;   /* critical: never eat clicks */
  will-change: transform;
  z-index: 9999;
}
const dot = document.querySelector('.cursor-dot');
let x = 0, y = 0, cx = 0, cy = 0;

addEventListener('pointermove', e => { x = e.clientX; y = e.clientY; });

function loop() {
  cx += (x - cx) * 0.18;          // easing toward the pointer
  cy += (y - cy) * 0.18;
  dot.style.transform = `translate3d(${cx - 16}px, ${cy - 16}px, 0)`;
  requestAnimationFrame(loop);
}
loop();

Three things in that snippet are doing real work. pointer-events: none stops the follower from intercepting clicks, which is the bug that makes an entire site unusable if you forget it. Driving position with transform rather than top and left keeps the movement on the compositor instead of forcing layout on every frame. And updating inside requestAnimationFrame rather than directly in the event handler caps the work at one update per frame no matter how fast the pointer events arrive.

The cost is real. You now have an element repainting every frame for the entire session. On a page already struggling, that shows up in interaction latency. Our notes on Elementor Core Web Vitals cover where that budget goes.

Touch Devices and the Pointer Media Query

A follower cursor on a touchscreen is worse than useless. There is no pointer to follow, so the element either sits frozen in a corner or jumps to the last tap. Gate the whole thing behind a media query that asks whether a fine pointer exists.

.cursor-dot { display: none; }

@media (pointer: fine) and (hover: hover) {
  .cursor-dot { display: block; }
  body { cursor: none; }
}

@media (prefers-reduced-motion: reduce) {
  .cursor-dot { display: none; }
  body { cursor: auto; }
}

Note that the default is hidden and the query turns it on, rather than the other way round. If the query fails to match for a reason you did not anticipate, the user gets a normal working cursor instead of a broken one.

There is also a platform limit worth knowing. MDN records that iPadOS supports pointer devices like trackpads and mice, that the iPad cursor is displayed as a circle by default, and that the only supported value that will change the appearance of the pointer is text. So on iPadOS your custom cursor image will not apply regardless of what you do.

Accessibility: Never Hide the Cursor Globally

The follower pattern requires cursor: none on the body, and that is where it gets risky. The system cursor is an assistive feature for a lot of people. Users with low vision often enlarge it at the OS level, and hiding it replaces something tuned to their needs with a decorative circle that is usually thinner and lower contrast.

  • Respect prefers-reduced-motion by restoring the real cursor, as in the snippet above. A lagging follower is exactly the kind of motion that setting exists for.
  • Never apply cursor: none to form fields. Text entry depends on the caret and the I-beam.
  • Keep focus styles completely independent. Keyboard users never see the cursor at all, so it must never be the only affordance.
  • Make the follower at least as visible as the pointer it replaced. A 1 pixel light grey ring on a white page is a downgrade.

Doing It in Elementor

For the simple case, the CSS property needs no plugin at all. Upload the image to the media library, copy its URL, and add one rule in the Custom CSS panel of the section or the widget. Scope it to a class rather than body so you can turn it off per template.

selector .interactive-zone {
  cursor: url("/wp-content/uploads/2026/09/cursor.png") 16 16, pointer;
}

For the follower version you need the markup, the CSS, and the script together, which is a custom HTML widget plus a bit of care about where the script runs. Keep the element outside any container that has overflow: hidden, or it will be clipped at the section boundary. The Plus Addons for Elementor is worth reaching for here mainly because the hover and interaction states are what make a follower cursor feel intentional rather than decorative, and wiring those consistently by hand across many widgets is the slow part.

Whichever route you take, define the cursor image once as a global so a redesign does not leave three different pointers scattered across templates. Building a reusable design system in Elementor V4 covers that setup.

When to Skip It Entirely

Custom cursors earn their place on portfolio sites, agency work, product launches, interactive canvases, and anywhere the visit is itself the experience. They cost you almost nothing there and they are memorable.

Skip them on checkout, forms, dashboards, documentation, and anything people use under time pressure or repeatedly. In those contexts the pointer is a tool, the user has already learned its exact behaviour, and replacing it makes the interface slower to use in exchange for a stylistic note nobody asked for.

A reasonable middle position is to scope the effect to one section. A custom cursor inside a portfolio gallery, with the normal pointer everywhere else, gets you the character without taxing the parts of the site that have work to do.

Frequently Asked Questions

Why Is My Custom Cursor Not Showing?

Three causes account for nearly all of it. The image is larger than the 128 by 128 ceiling that Firefox and Chromium apply, so the declaration is ignored. The fallback keyword is missing, so the whole declaration is invalid. Or the image path is wrong, in which case the fallback keyword is what you are seeing.

What Size Should a Custom Cursor Be?

32 by 32 pixels. MDN recommends that size explicitly, and it is comfortably under the 128 by 128 default ceiling in Firefox and Chromium. Anything larger than the ceiling is ignored rather than scaled.

Can I Use an Animated GIF as a Cursor?

Not reliably. The specification requires support for non-animated formats and only says user agents should support animated ones, so it is optional. If you need motion, use the JavaScript follower and animate a normal DOM element.

Do Custom Cursors Work on Mobile?

No, and they should be disabled there. Touchscreens have no persistent pointer. On iPadOS with a trackpad, MDN notes the cursor shows as a circle and only the text value changes its appearance, so custom images do not apply. Gate the effect behind @media (pointer: fine).

Are Custom Cursors Bad for Accessibility?

Replacing the cursor image is low risk. Hiding it with cursor: none for a JavaScript follower is higher risk, because it overrides OS-level cursor size and contrast settings that some users depend on. Honour prefers-reduced-motion, keep the system cursor on form fields, and make the replacement at least as visible.

Suggested Reading

Related Frequently Asked Questions

Why is my custom cursor not showing up in CSS?

The usual culprits are a missing fallback keyword, an image that is too large, or a bad file path. The page notes that Firefox and Chromium commonly ignore cursor images above 128 by 128 pixels, and the declaration is also dropped if you leave out the fallback value. A practical fix is to keep the image at 32 by 32 and always end the rule with something like `auto` or `pointer` so the browser still has a valid cursor to use.

What size should a custom cursor be for reliable CSS support?

32 by 32 pixels is the safe target. The page says MDN recommends that size, while Firefox and Chromium default to a 128 by 128 ceiling. Bigger images are not scaled down, they are usually ignored. That matters because oversized cursors fail silently, so a design that looks fine in one browser can disappear in another. If you want a larger visual effect, the page points to a JavaScript follower instead of pushing the CSS property past its limit.

Can I use an animated GIF as a custom cursor?

Not reliably. The page says the specification requires support for PNG and certain SVG files, while animated formats are only optional for user agents. That means an animated GIF may work in some cases, but it is not something you can count on across browsers. If motion is part of the design, the safer route is the JavaScript follower cursor described in the article, because that animates a normal DOM element instead of relying on cursor-image support.

Do custom cursors work on mobile or iPadOS?

Touch devices are a bad fit for custom cursors because there is no persistent pointer to follow. The page also notes that on iPadOS with trackpads or mice, the cursor appears as a circle by default and only the text value changes its appearance, so custom images do not apply there. The practical rule is to gate the effect behind `@media (pointer: fine)` and keep it off on touch-first devices.

What is the safest way to add a custom cursor in Elementor?

For a simple image cursor, scope it to one section or widget with Custom CSS instead of applying it globally. The page shows using a class on an interactive zone and setting `cursor: url(... ) 16 16, pointer;`, which keeps the effect contained and avoids breaking unrelated parts of the site. The Plus Addons for Elementor, The Plus Addons, Plus Addons for Elementor, POSIMYTH fits better when you want hover and interaction states around a follower-style effect inside Elementor.

When should I skip custom cursors entirely?

Skip them anywhere people need speed and precision: checkout flows, forms, dashboards, documentation, and repeated tasks. The page’s point is that in those contexts the pointer is already learned muscle memory, so replacing it adds style without helping usability. A better compromise is to limit the effect to one section like a portfolio gallery. That gives you personality where it matters without making high-friction pages harder to use.

Last reviewed: September 2, 2026