Skip to content

Crosshair Cursor Readout

A drafting-tool crosshair with live X/Y coordinates that trails the pointer.

Freebrutalistminimaleditorial
Category
Components
Added
2026-09-20
Deps
none
Updated
2026-09-20
Preview
Hover to track
X 000 · Y 000

Props

Coordinate readout
Center ring
No runtime dependencies
"use client";

import { useRef, useState } from "react";

export function CursorCrosshairReadout({
  children,
  showReadout = true,
  showRings = true,
  lineStyle = "dashed",
}: {
  children?: React.ReactNode;
  showReadout?: boolean;
  /** The centre ring and its dot. Off leaves only the two guides. */
  showRings?: boolean;
  lineStyle?: "dashed" | "solid";
}) {
  const boxRef = useRef<HTMLDivElement>(null);
  const frame = useRef<number | null>(null);
  const [coords, setCoords] = useState({ x: 0, y: 0 });
  const [active, setActive] = useState(false);

  function handleMove(e: React.MouseEvent<HTMLDivElement>) {
    const el = boxRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    const x = Math.round(e.clientX - rect.left);
    const y = Math.round(e.clientY - rect.top);

    // Lines follow synchronously via custom properties — no re-render.
    el.style.setProperty("--cx", x + "px");
    el.style.setProperty("--cy", y + "px");

    // Only the numeric readout needs React, throttled to one update a frame.
    if (frame.current !== null) return;
    frame.current = requestAnimationFrame(() => {
      frame.current = null;
      setCoords({ x, y });
    });
  }

  const border = lineStyle === "solid" ? "border-solid" : "border-dashed";

  return (
    <div
      ref={boxRef}
      onMouseMove={handleMove}
      onMouseEnter={() => setActive(true)}
      onMouseLeave={() => setActive(false)}
      className="relative cursor-none overflow-hidden"
      style={{ "--cx": "50%", "--cy": "50%" } as React.CSSProperties}
    >
      {children}

      <div
        className="pointer-events-none absolute inset-0 transition-opacity duration-200"
        style={{ opacity: active ? 1 : 0 }}
      >
        <div
          className={"absolute left-0 w-full border-t " + border + " border-white/40"}
          style={{ top: "var(--cy)" }}
        />
        <div
          className={"absolute top-0 h-full border-l " + border + " border-white/40"}
          style={{ left: "var(--cx)" }}
        />

        {showRings && (
          <>
            <div
              className="absolute size-10 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/35"
              style={{ left: "var(--cx)", top: "var(--cy)" }}
            />
            <div
              className="absolute size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500"
              style={{ left: "var(--cx)", top: "var(--cy)" }}
            />
          </>
        )}

        {showReadout && (
          <div
            className="absolute translate-x-4 translate-y-4 rounded border border-white/20 bg-black px-2 py-1 font-mono text-[10px] tabular-nums text-white"
            style={{ left: "var(--cx)", top: "var(--cy)" }}
          >
            X {String(coords.x).padStart(3, "0")} · Y {String(coords.y).padStart(3, "0")}
          </div>
        )}
      </div>
    </div>
  );
}
Notes

About this component

Most custom cursors soften the pointer: a blob that lags behind, a trail that smears. This one does the opposite, and that is the reason to reach for it. The guides snap to the pointer with zero interpolation, because the effect is borrowed from CAD and drafting software, where lag would be a defect rather than a flourish — it reads as precision instead of personality, which makes it the right choice over this library's Liquid Blob Cursor on anything data-dense: a chart, a map, an image the user is inspecting rather than admiring. The implementation detail worth copying even if you never use the visual: pointer position drives the guides through CSS custom properties written from a ref, so moving the mouse costs one style recalculation rather than a React render of the whole container. Only the numeric readout — the one part that must genuinely be text — goes through state, throttled to one update per frame. The caveat: `cursor-none` hides the real pointer, so this needs a visible focus path for keyboard users and should be scoped to a container, never the whole page.

Curator’s note

Built for viberdy 2.0 as the 'machined' counterpart to the library's softer cursor followers — no spring, no lag, deliberately instant.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
New

A contextual chip that trails the pointer and names whatever it is over.

cursorlabelspring
Trending
New

A glass lens that follows the pointer and magnifies the live DOM beneath it.

cursorlensmagnify
Featured
New

Snapping Cursor

components

A cursor dot that latches onto interactive elements and takes their exact shape.

cursormagneticsnap