Cursors✨ Premium

Cursor Text

A fixed 12 px label offset 15 px below the pointer, rewriting the mouse's local coordinates on every move — “X: 228 Y: 180”, rounded to the pixel, measured from the zone's top-left corner. The native cursor is hidden: the label is the only landmark.

JSInfo

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

Editor artboard — live position readout — Cursor Text example 1

① Editor artboard — live position readout

WhenDesign tool interface: rulers, an artboard, one shape placed. The label gives the pointer position in pixels.
WhyThis is the code's literal use: local, rounded coordinates that follow the hand. The monospace label read 15 px from the pointer never hides what you are aligning.
SettingsColor #a5b4fc (7.8:1), offset translate(18px, 18px) to clear the shape's handle, cursor: none removed to keep the system crosshair.
Seating plan — locating a seat — Cursor Text example 2

② Seating plan — locating a seat

WhenSeat picker for a venue or an open-plan office: a grid of seats, a legend.
WhyCoordinates become a row-and-column landmark; the hidden cursor and the light label prevent the arrow from covering a 12 px seat.
SettingsText replaced by Row ${…} · Seat ${…} derived from the coordinates, background rgba(15,15,26,.9), color #c7d2fe.
Analytics chart — position on the axes — Cursor Text example 3

③ Analytics chart — position on the axes

WhenDashboard: a bar or area chart, axes, a period. The label reads the pointer position.
WhyA probe role: the reader sees where they point even before the charting library's tooltip appears; the light label doesn't compete with the displayed values.
Settingsui-monospace font at 11 px, border removed, offset reduced to translate(12px, -28px) to read above the pointer.

How it works

A single element carries the effect: #cursorText (.custom-cursor.cursor-text-display). The .custom-cursor class takes it out of the flow — position: fixed, pointer-events: none, z-index: 9999, display: none — and .cursor-text-display dresses it: font: 600 .75rem Inter, monospace, color #6366f1, background rgba(99,102,241,.1), 1 px border rgba(99,102,241,.3), padding: 4px 8px, border-radius: 4px, white-space: nowrap and, above all, transform: translate(15px, 15px), the offset that keeps it away from the hot spot.

The #zone-text area gets three listeners. mouseenter adds the cursor-active class to the zone — this time the rule does exist in the shipped CSS: .cursor-active { cursor: none !important }, the system arrow disappears (measured: cursor: none) — and switches the label to display: block. mouseleave undoes both. mousemove does the work: localX = Math.round(clientX − rect.left), same for Y, then textContent = `X: ${localX} Y: ${localY}` and left = clientX + 'px', top = clientY + 'px'.

The written coordinates are local to the zone (0, 0 = its top-left corner), recomputed through getBoundingClientRect() on every event — so they stay right even if the page has scrolled or the zone has moved. Positioning, on the other hand, uses viewport coordinates (clientX/Y, consistent with position: fixed). Measured: for a pointer at (228, 180) inside the zone, the text reads exactly “X: 228 Y: 180” and the label's top-left corner sits at +15 px, +15 px from the pointer; the label measures 112 × 24 px.

No requestAnimationFrame, no smoothing: the label is glued to the pointer, 0 rAF calls/s outside hover; the per-event cost is one textContent write (relayout of the label alone). The shipped JS file contains only this block (“Zone 9: Cursor Text”), nothing from another effect. The .cursor-zone rule defines its five variables locally: the demo zone renders correctly on a blank page.

Accessibility

  • Insufficient label contrast: #6366f1 on its background (rgba(99,102,241,.1) composited over #1a1a24, i.e. #212238) = 3.5:1 for 12 px text, below the 4.5:1 AA threshold. Switch the color to #a5b4fc (7.8:1) or #c7d2fe (10.4:1); #818cf8 (5.2:1) stays closest to the original.
  • cursor: none makes the label the only pointer indicator, yet it is offset 15 px down-right: nothing marks the exact hot spot. For a precision interface, remove the .cursor-active rule (the arrow returns, the label rides along) or add a 4 px ::before at translate(-15px, -15px) that sits on the pointer.
  • Screen readers: the div changes text on every move. It has no aria-live, so nothing is announced, but it remains in the accessibility tree: add aria-hidden="true" so assistive technology ignores it entirely. If the coordinates carry meaning (a position picker), also expose them in a field or an output element.
  • Touch: measured with touch emulation, a tap inside the zone shows the label frozen on the tap coordinates and leaves cursor-active in place even after a tap outside the zone (no mouseleave). Gate the initialization on matchMedia('(hover: hover) and (pointer: fine)').matches.
  • Keyboard: no equivalent, the label never shows — acceptable as long as it is only a convenience. No autonomous motion: prefers-reduced-motion has nothing to disable. z-index: 9999: the label floats above open modals and menus.

Browser compatibility

Requires ES2015 (template literals, arrow functions, const), classList, getBoundingClientRect and the zone's CSS variables. Zero dependencies.

Chrome 49+✓ Full
Firefox 45+✓ Full
Safari 10+✓ Full
Edge 15+✓ Full
Mobile iOS✓ Inactive (label frozen on tap)
Android Chrome✓ Inactive (label frozen on tap)

Without JavaScript, the label stays display: none and the system arrow is kept: the zone is an ordinary box. Without CSS variables, the zone loses its background and radius; the label, which uses none, stays intact.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="cursor-zone" id="zone-text">
  Hover this area
  <span class="info-text">Following label</span>
</div>
<!-- This div IS the cursor: the script moves it and shows it.
     Without it the script bails out and the effect renders nothing. -->
<div class="custom-cursor cursor-text-display" id="cursorText">X: 0 Y: 0</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Offset (CSS — .cursor-text-display transform) translate(15px, 15px) Label position relative to the hot spot. Negative values place it above or to the left; 0 centers it on the pointer and hides what it targets.
Content (JS — cursorText.textContent = …) X: ${localX} Y: ${localY} What the label reads. A fixed text (“View”, “Drag”) is set once; for units, divide by rect.width for percentages.
Text color (CSS — .cursor-text-display color) #6366f1 3.5:1 on the shipped background. #a5b4fc or #c7d2fe pass the 4.5:1 threshold without changing the hue.
Font and size (CSS — .cursor-text-display font) 600 .75rem Inter, monospace Inter is not shipped: the stack falls back to monospace. A deliberate monospace (ui-monospace, Menlo) avoids width jumps as digits change.
Background and border (CSS — .cursor-text-display background, border) rgba(99,102,241,.1) / 1px rgba(99,102,241,.3) Chip styling. An opaque background (#0f0f1a) guarantees legibility over an image; without a border, the label becomes plain floating text.
Native cursor hiding (CSS — .cursor-active) cursor: none !important Delete the rule to keep the system arrow and only add the label — recommended as soon as the zone holds precision targets.
Coordinate rounding (JS — Math.round) to the pixel Math.round(x * 10) / 10 for a tenth of a pixel (useful when zoomed), or Math.floor to stay in the hovered cell of a grid.

FAQ

Replace the line cursorText.textContent = `X: ${localX} Y: ${localY}` with the text you want, or set it once in the HTML and delete that line along with the two now-useless Math.round. For a text that depends on the hovered element, read e.target.closest('[data-cursor-text]') and copy its attribute — the rest of the code (display, position, offset) is unchanged.
Because an ancestor of #cursorText has transform, filter, perspective, will-change: transform or contain: paint: it becomes the containing block of the position: fixed element, and left = clientX is then measured from that ancestor, no longer from the window. The shipped code places the label inside the demo zone; when integrating, move it right before </body>, outside any transformed container.
To the listened zone: rect = zoneText.getBoundingClientRect(), then clientX − rect.left — (0, 0) is the zone's top-left corner, regardless of scrolling. For percentages: Math.round((clientX − rect.left) / rect.width × 100). For document coordinates: e.pageX and e.pageY directly. The label's positioning must stay in clientX/Y, since it is fixed.