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.
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
<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> .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; }
} (() => {
// 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.