Crosshair Cursor

Two hairlines span the whole area and meet at the pointer, with a live coordinate readout, like a design tool ruler.

  • JS
  • Cursor
  • Pointer
  • Added
Move inside

AI prompt

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

Demo.tsx
'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>
  )
}
demo.module.css
.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; }

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.