Emoji Cursor
Five emojis in an array, an index that advances on every click: the moment the mouse enters the zone, the system cursor disappears and a Unicode character takes its place, repositioned on every mousemove to match the pointer's exact coordinates.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Cursors has 26 effects, including 1 free. Explore the category →
3 usage examples



How it works
The effect relies on two separate actors: the detection zone (#zone-emoji, 300 px tall) and a floating element (#cursorEmoji) positioned outside the normal document flow. The moment mouseenter fires on the zone, two things happen at once: the zone receives the cursor-active class — the CSS hook that hides the native cursor via cursor: none — and the emoji element switches from display: none to display: block. The symmetric mouseleave reverses both: class removed, element hidden. The system cursor reappears immediately on exit, no delay.
Position tracking is direct, with no intermediate computation. On every mousemove, the script writes e.clientX into style.left and e.clientY into style.top of the emoji element. No threshold, no easing force: the emoji tracks the pointer pixel for pixel in the viewport's coordinate space. For these absolute coordinates to land correctly, the element must be set to position: fixed in the CSS — that keeps it positioned correctly regardless of scroll depth.
The third listener, click, attached to the zone, handles the rotation. An integer index initialized at 0 advances on each click using the modulus operator: (emojiIndex + 1) % emojis.length. With a five-entry array — ✨ → 🚀 → 💫 → ⭐ → 🔥 → ✨… — the loop never ends. The script updates the element's textContent with the current entry. The index persists between zone entries (but resets to 0 on page reload), so the visitor resumes on the last clicked emoji if they re-enter the zone.
Accessibility
- The
#cursorEmojielement carries a Unicode character that screen readers interpret: '✨' may be announced as 'sparkles' depending on the synthesis engine. Addaria-hidden="true"to this element before deployment — it is a pointer decoration, not information to be read aloud. - The emoji rotation fires on
clickon the zone — not from the keyboard. A keyboard user can interact with child elements inside the zone, but will never see or trigger the symbol change. Never make information or action feedback depend on this mechanism. - On touch screens,
mouseenter,mousemoveandmouseleavedo not fire in normal use. The emoji cursor stays hidden and the zone behaves like a plain block. Some mobile browsers emit a simulatedmouseenteron tap — test that behavior if the zone contains tappable elements. - prefers-reduced-motion has no direct impact: the effect produces no CSS animation in the strict sense. If you add a transition to the emoji's appearance or repositioning, that preference should govern it.
Browser compatibility
The JavaScript syntax is ES2015: arrow functions, const, modulus on a variable — IE 11 halts on a syntax error. The real compatibility factor lies in the OS emoji rendering engine: Apple Color Emoji (macOS/iOS), Segoe UI Emoji (Windows) and Noto Color Emoji (Android/Linux) can produce radically different drawings for the same Unicode character.
Without JavaScript, <code>#cursorEmoji</code> stays at <code>display: none</code> and the native cursor works normally. On an ES5 engine, a syntax error blocks execution; the zone remains visible and readable. The effect is purely additive: nothing breaks without it.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| The emojis array | ['✨', '🚀', '💫', '⭐', '🔥'] — 5 entries | This is the only content list in the script. Replace the characters with your own: the first displays on zone entry (index 0), the rest cycle on click. The modulus adjusts automatically to any length — 3, 8 entries, or 1 alone for a fixed cursor with no rotation. |
| Initial emoji (emojiIndex = 0) | index 0 → '✨' | The index is reset to 0 on page reload, not on each zone entry: if the visitor clicks, exits, then re-enters, the cursor resumes from the last emoji shown. To consistently start on a different symbol, change the index's initial value. |
| Positioning offset | emoji's top-left corner = exact cursor coordinates | The emoji is placed by its top-left corner at the pointer's coordinates, so the cursor points into the character's corner. A fixed pixel offset shifts the anchor point — useful to center the emoji visually on the cursor tip. |
| Activation zone (#zone-emoji) | a 300 px tall div | The listeners are attached to the element targeted by getElementById('zone-emoji'). Swap it for any container in your page. To cover the entire document, target document.body and add cursor: none on body in the CSS. |
| Display toggle logic (display block / none) | instant toggle on mouseenter / mouseleave | The emoji appears and disappears without any transition. For a fade, replace the display toggle with an opacity toggle and a CSS transition, keeping display: block permanently. Add pointer-events: none when the element is invisible — otherwise it intercepts clicks. |
| cursor-active class (on #zone-emoji) | added on mouseenter, removed on mouseleave | This class carries the cursor: none rule on the zone. If a nested element (a link, a field) needs to restore the native cursor, add cursor: auto directly on that element — it overrides the parent rule without touching the JavaScript. |
FAQ
document.body instead of the dedicated div and attach the listeners to it. Add cursor: none on body in the CSS. The #cursorEmoji element must stay in position: fixed to reposition correctly on scroll. Keeping mouseenter/mouseleave on body makes the emoji disappear when the mouse leaves the browser window — a coherent behavior.style.left and style.top) with no heavy computation — no Math.sqrt, no getBoundingClientRect(). On a position: fixed element outside the document flow, the browser handles the update without a page reflow. A requestAnimationFrame wrapper can cap write frequency if needed — but it is rarely necessary for this effect.