Cursor effect

Custom image cursor in CSS: SVG arrow, hotspot, fallback

Two inline SVG cursors, an arrow and a dot for links, set with cursor: url() and a keyword fallback. Text fields keep their normal I-beam. An optional script adds a small ripple where you click.

The effect running on this page, with exactly the code below. Move your mouse inside the stage: the effect stays inside it. On a touch screen, or with reduced motion turned on, it holds back, as it should.

Usage

When to use it

  • Portfolio and brand sites where a custom arrow is part of the look.
  • Game and kids' sites where a themed cursor adds character.
  • Tools that need a specific pointer shape, such as a pen or eyedropper.
  • Scope it to a section, keep each image at 32x32, and always end with a keyword.

Code

Copy the effect

HTML
<section class="custom-cursor-area">
  <p>Move over this area to see the custom arrow.</p>
  <p><a href="#">Links get the blue dot</a></p>
  <label>Fields keep the I-beam <input type="text"></label>
</section>
CSS
.custom-cursor-area {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M4 2v22l5.5-5 3.5 8.5 3.5-1.5-3.5-8H20z' fill='%2315161a' stroke='%23fff' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E") 4 2, auto;
}
.custom-cursor-area :is(a, button, [role="button"], summary) {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='8' fill='%232d5bff' stroke='%23fff' stroke-width='2'/%3E%3C/svg%3E") 16 16, pointer;
}
.custom-cursor-area :is(input, textarea, select, [contenteditable="true"]) {
  cursor: auto;
}
.click-ripple {
  position: fixed;
  z-index: 9999;
  width: 24px;
  height: 24px;
  margin: -12px 0 0 -12px;
  box-sizing: border-box;
  border: 2px solid #2d5bff;
  border-radius: 50%;
  pointer-events: none;
  animation: click-ripple 0.45s ease-out forwards;
}
@keyframes click-ripple {
  from { transform: scale(0.4); opacity: 1; }
  to { transform: scale(1.6); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .click-ripple { display: none; }
}
JavaScript
(() => {
  // Optional: a small ripple where the mouse clicks. The cursors themselves are pure CSS.
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

  document.querySelectorAll('.custom-cursor-area').forEach((area) => {
    area.addEventListener('pointerdown', (event) => {
      if (event.pointerType !== 'mouse' || reduceMotion.matches) return;
      const ripple = document.createElement('span');
      ripple.className = 'click-ripple';
      ripple.setAttribute('aria-hidden', 'true');
      ripple.style.left = `${event.clientX}px`;
      ripple.style.top = `${event.clientY}px`;
      document.body.appendChild(ripple);
      ripple.addEventListener('animationend', () => ripple.remove());
      setTimeout(() => ripple.remove(), 1000); // safety net if the animation never runs
    });
  });
})();

Accessibility

Motion, touch and screen readers

Reduced motion

The cursors themselves never animate: they are still images that move only when the pointer does. The one moving part is the click ripple. With prefers-reduced-motion: reduce, the script does not create ripples and the CSS hides any that exist.

Accessibility notes

  • Each url() ends with a keyword (auto, pointer), so visitors get a normal cursor if the image fails or the browser rejects it.
  • Both images have a white outline, so they stay visible on dark and light backgrounds.
  • Form fields go back to cursor: auto, which shows the I-beam people expect when typing.
  • Visitors who set a large system pointer get your 32x32 image instead. Keep the custom cursor to one section if your audience includes people who rely on pointer size.
  • The ripple has aria-hidden="true" and pointer-events: none.

Site builders

Webflow, Framer and React

Webflow

In Webflow, paste the CSS into a <style> tag in the page or site custom code, then give your section the class custom-cursor-area. For an image file instead of inline SVG, upload it to Assets, copy its URL and use it inside url(). Webflow's Style panel (Effects > Cursor) only offers the keywords, not images.

Framer

Framer's Cursor settings let you pick a keyword or attach a component as a custom cursor. For an image cursor with an exact hotspot, add the CSS in Site settings > General > Custom Code (in <head>) and give the section the class through an Embed or a code component.

React

The CSS works as-is in a global stylesheet or a CSS module (use :global for the class if needed). The ripple script fits in a useEffect on the section's ref, with the listener removed in the cleanup. Nothing here needs state.

FAQ

Questions

Why does my custom cursor turn back into the normal arrow near the edge of the page?
Chrome 156 and later, and Safari, switch to the fallback when the cursor image would stick out of the visible page. With a 16 16 hotspot, that happens within 16 px of an edge. Firefox and older Chrome only do this for images larger than 32x32.
Can I use a PNG instead of inline SVG?
Yes: cursor: url("/cursors/arrow.png") 4 2, auto;. Use a 32x32 PNG and, for sharp screens, an SVG first in the list with the PNG as the second choice.
Where do the numbers 4 2 come from?
They are the hotspot: the pixel, counted from the image's top-left corner, that does the clicking. In this arrow the tip sits at x 4, y 2.