Crosshair Cursor Readout
A drafting-tool crosshair with live X/Y coordinates that trails the pointer.
- Category
- Components
- Added
- 2026-09-20
- Deps
- none
- Updated
- 2026-09-20
Props
"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>
);
}
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.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Cursor Label Follower
componentsA contextual chip that trails the pointer and names whatever it is over.
Cursor Lens Magnifier
componentsA glass lens that follows the pointer and magnifies the live DOM beneath it.
Snapping Cursor
componentsA cursor dot that latches onto interactive elements and takes their exact shape.