Crosshair Cursor
Two hairlines span the whole area and meet at the pointer, with a live coordinate readout, like a design tool ruler.
Create a crosshair cursor inside a bounded area in React, no libraries. Hide the native cursor in the area with cursor: none. Render a full-width horizontal hairline and a full-height vertical hairline (1px, accent colour at 60% opacity) and a small mono readout label. On pointermove, read the pointer position relative to the area and set translate on the lines and the label directly through refs in a requestAnimationFrame, writing "x, y" in whole pixels into the label. Show everything on pointerenter and fade it out on pointerleave with a 150ms opacity transition. Keep the label from overflowing the right and bottom edges by flipping its offset. Mark the lines aria-hidden. Show a centred hint while the pointer is outside. The hint reads "Move inside".
Source
'use client'
import { useRef } from 'react'
import s from './demo.module.css'
export default function CrosshairCursor({ labels }: { labels: Record<string, string> }) {
const area = useRef<HTMLDivElement>(null)
const h = useRef<HTMLSpanElement>(null)
const v = useRef<HTMLSpanElement>(null)
const tag = useRef<HTMLSpanElement>(null)
const raf = useRef(0)
const onMove = (e: React.PointerEvent<HTMLDivElement>) => {
const r = e.currentTarget.getBoundingClientRect()
const x = Math.round(e.clientX - r.left)
const y = Math.round(e.clientY - r.top)
cancelAnimationFrame(raf.current)
raf.current = requestAnimationFrame(() => {
h.current!.style.translate = `0 ${y}px`
v.current!.style.translate = `${x}px 0`
const t = tag.current!
const dx = x > r.width - 80 ? -t.offsetWidth - 8 : 8
const dy = y > r.height - 30 ? -t.offsetHeight - 8 : 8
t.style.translate = `${x + dx}px ${y + dy}px`
t.textContent = `${x}, ${y}`
})
}
return (
<div
ref={area}
className={s.area}
onPointerMove={onMove}
onPointerEnter={() => area.current?.setAttribute('data-on', '')}
onPointerLeave={() => area.current?.removeAttribute('data-on')}
>
<span className={s.hint}>{labels.hint}</span>
<span ref={h} className={s.h} aria-hidden="true" />
<span ref={v} className={s.v} aria-hidden="true" />
<span ref={tag} className={s.tag} aria-hidden="true" />
</div>
)
}
.area {
position: absolute;
inset: 0;
display: grid;
place-items: center;
overflow: hidden;
cursor: none;
background:
repeating-linear-gradient(90deg, var(--stage-surface) 0 1px, transparent 1px 24px),
repeating-linear-gradient(0deg, var(--stage-surface) 0 1px, transparent 1px 24px),
var(--stage-bg);
}
.hint {
font: 500 var(--text-xs) / 1 var(--font-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--stage-muted);
transition: opacity 150ms;
}
.h, .v, .tag {
position: absolute;
left: 0;
top: 0;
opacity: 0;
pointer-events: none;
transition: opacity 150ms;
}
.h { width: 100%; height: 1px; background: color-mix(in oklch, var(--stage-accent) 60%, transparent); }
.v { width: 1px; height: 100%; background: color-mix(in oklch, var(--stage-accent) 60%, transparent); }
.tag {
padding: 3px 6px;
border-radius: 4px;
background: var(--stage-accent);
color: var(--stage-accent-ink);
font: 600 0.6875rem / 1 var(--font-mono);
white-space: nowrap;
}
.area[data-on] .h,
.area[data-on] .v,
.area[data-on] .tag { opacity: 1; }
.area[data-on] .hint { opacity: 0; }