Crosshair Cursor Readout
A drafting-tool crosshair for any surface: hairline guides with live X/Y tags on ticked rulers, snapping to element corners, edges and centres with their dimensions drawn, click-to-pin distance measuring, keyboard nudging and a touch handle. Patrols the snap targets on its own at rest. No dependencies.
DOM + CSS0 deps1 file · 31 KB TSXreduced-motion aware
Props
"use client";
import { useEffect, useId, useRef } from "react";
import type { CSSProperties, ReactNode } from "react";
type Box = { l: number; t: number; r: number; b: number; name: string };
type Cfg = { snap: boolean; measure: boolean; snapRadius: number; snapSelector: string; idlePatrol: boolean };
type Pt = [number, number];
// fx, fy (0..1 across the box), short code, spoken name
const SPOTS: ReadonlyArray<readonly [number, number, string, string]> = [
[0, 0, "NW", "top left corner"],
[0.5, 0, "N", "top edge"],
[1, 0, "NE", "top right corner"],
[0, 0.5, "W", "left edge"],
[0.5, 0.5, "C", "center"],
[1, 0.5, "E", "right edge"],
[0, 1, "SW", "bottom left corner"],
[0.5, 1, "S", "bottom edge"],
[1, 1, "SE", "bottom right corner"],
];
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
const INTERACTIVE = "a,button,input,select,textarea,label,summary,[role=button],[role=link],[contenteditable]";
const MONO = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));
const pad = (n: number) => String(Math.round(n)).padStart(3, "0");
const sgn = (n: number) => (n < 0 ? "−" : "+") + Math.abs(Math.round(n));
const TAG: CSSProperties = {
position: "absolute",
font: "600 9.5px/14px " + MONO,
letterSpacing: "0.04em",
fontVariantNumeric: "tabular-nums",
color: "#fff",
background: "#0a0b0e",
boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.16)",
borderRadius: 3,
padding: "0 4px",
whiteSpace: "nowrap",
};
const DIM: CSSProperties = {
position: "absolute",
font: "600 9.5px/15px " + MONO,
fontVariantNumeric: "tabular-nums",
color: "#fff",
background: "var(--ccr-a)",
borderRadius: 3,
padding: "0 5px",
whiteSpace: "nowrap",
};
const PILL: CSSProperties = {
font: "500 10px/1 " + MONO,
letterSpacing: "0.12em",
textTransform: "uppercase",
color: "rgba(255,255,255,0.78)",
background: "rgba(8,9,11,0.82)",
boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.14)",
borderRadius: 999,
padding: "6px 10px",
};
/**
* A drafting-table cursor for a bounded surface. Mark elements with data-snap
* (or pass snapSelector) and the crosshair snaps to their corners, edges and
* centre while their dimensions are drawn. Click (or press Enter / the Pin
* button) to drop an origin and read the offset and distance from it. With no
* pointer it patrols the snap targets by itself.
*/
export function CursorCrosshairReadout({
children,
showReadout = true,
showRings = true,
lineStyle = "dashed",
showRulers = true,
snap = true,
measure = false,
idlePatrol = false,
snapRadius = 14,
snapSelector = "[data-snap]",
accent = "#3b82f6",
className,
}: {
children?: ReactNode;
showReadout?: boolean;
/** The centre reticle and its dot. Off leaves only the two guides. */
showRings?: boolean;
lineStyle?: "dashed" | "solid";
/** Ticked rulers on the top and left edges, with the live X / Y tags. */
showRulers?: boolean;
/** Snap to the corners, edge midpoints and centre of snap targets. */
snap?: boolean;
/** Click, Enter or the Pin button drops an origin and measures from it. */
measure?: boolean;
/** With no pointer, patrol the snap targets by itself. Off parks the crosshair. */
idlePatrol?: boolean;
/** Pointer distance (px) at which the crosshair snaps. */
snapRadius?: number;
/** CSS selector for snap targets, matched inside the surface. */
snapSelector?: string;
/** Hex colour for the centre dot, snap boxes and dimension tags. */
accent?: string;
className?: string;
}) {
const rootRef = useRef<HTMLDivElement>(null);
const ovRef = useRef<HTMLDivElement>(null);
const cfg = useRef<Cfg>({ snap, measure, snapRadius, snapSelector, idlePatrol });
const redraw = useRef<() => void>(() => {});
const descId = "ccr" + useId().replace(/[^a-zA-Z0-9]/g, "");
const acc = HEX.test(accent) ? accent : "#3b82f6";
const line = lineStyle === "solid" ? "solid" : "dashed";
useEffect(() => {
cfg.current = { snap, measure, snapRadius, snapSelector, idlePatrol };
redraw.current();
}, [snap, measure, snapRadius, snapSelector, idlePatrol]);
useEffect(() => {
const root = rootRef.current;
const ov = ovRef.current;
if (!root || !ov) return;
const q = (k: string) => ov.querySelector<HTMLElement>('[data-ccr="' + k + '"]');
const eX = q("x");
const eY = q("y");
const chip = q("chip");
const rowB = q("rb");
const rowC = q("rc");
const xTag = q("xtag");
const yTag = q("ytag");
const bx = q("box");
const bw = q("bw");
const bh = q("bh");
const bn = q("bn");
const mid = q("mid");
const size = q("size");
const pinBtn = q("pin");
const grip = q("grip");
const svg = ov.querySelector<SVGSVGElement>("svg");
const sLine = ov.querySelector<SVGLineElement>("line");
const sMark = ov.querySelector<SVGRectElement>("rect");
const live = root.querySelector<HTMLElement>("[data-ccr-live]");
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
let reduced = mq.matches;
let visible = true;
let docHidden = document.hidden;
let handle = 0;
let lastT = 0;
let W = 0;
let H = 0;
let boxes: Box[] = [];
// raw target / smoothed pointer / displayed (snapped) position
let tx = 0;
let ty = 0;
let px = -1;
let py = -1;
let dx = -1;
let dy = -1;
let hovering = false;
let focused = false;
let dragging = false;
let pressed = false;
let kb = false;
let kbIdx = -1;
let lastInput = -Infinity;
let settle = 0;
let wasEngaged = false;
let idleSince = -1;
let lastIdle = -1;
let anchor: Pt | null = null;
let pType = "mouse";
let grab: Pt | null = null;
let say = false;
let shown = false;
const HOLD = 4000;
const DWELL = 2100;
const engagedAt = (ts: number) =>
hovering || focused || dragging || ts - lastInput < (reduced ? Infinity : HOLD);
const measureBoxes = () => {
const rr = root.getBoundingClientRect();
W = root.offsetWidth;
H = root.offsetHeight;
const sx = rr.width > 0 ? W / rr.width : 1;
const sy = rr.height > 0 ? H / rr.height : 1;
boxes = [];
let list: NodeListOf<Element> | null = null;
try {
list = root.querySelectorAll(cfg.current.snapSelector);
} catch {
list = null;
}
if (!list) return;
list.forEach((el) => {
if (ov.contains(el)) return;
const r = el.getBoundingClientRect();
const l = (r.left - rr.left) * sx;
const t = (r.top - rr.top) * sy;
const rt = (r.right - rr.left) * sx;
const b = (r.bottom - rr.top) * sy;
if (rt - l < 4 || b - t < 4) return;
boxes.push({ l, t, r: rt, b, name: (el.getAttribute("data-snap") || el.tagName.toLowerCase()).slice(0, 24) });
});
};
const put = (el: HTMLElement | null, s: string) => {
if (el && el.textContent !== s) el.textContent = s;
};
const draw = (): boolean => {
const c = cfg.current;
const chipW = chip ? chip.offsetWidth : 0;
const chipH = chip ? chip.offsetHeight : 0;
let x = clamp(px, 0, W);
let y = clamp(py, 0, H);
let hit: Box | null = null;
let spot = -1;
if (c.snap) {
const R = kb ? 3 : Math.max(0, c.snapRadius);
let best = R + 0.001;
for (const b of boxes) {
for (let i = 0; i < SPOTS.length; i++) {
const s = SPOTS[i];
if (!s) continue;
const sx = b.l + (b.r - b.l) * s[0];
const sy = b.t + (b.b - b.t) * s[1];
const d = Math.hypot(sx - x, sy - y);
if (d < best) {
best = d;
hit = b;
spot = i;
}
}
}
if (hit && spot >= 0) {
const s = SPOTS[spot];
if (s) {
x = hit.l + (hit.r - hit.l) * s[0];
y = hit.t + (hit.b - hit.t) * s[1];
}
}
}
if (!hit) {
let area = Infinity;
for (const b of boxes) {
const a = (b.r - b.l) * (b.b - b.t);
if (x >= b.l && x <= b.r && y >= b.t && y <= b.b && a < area) {
area = a;
hit = b;
}
}
}
// ease only into a snap; free tracking stays instant
let pending = false;
if (dx < 0) {
dx = x;
dy = y;
} else {
const k = spot >= 0 && !reduced ? 0.55 : 1;
dx += (x - dx) * k;
dy += (y - dy) * k;
if (Math.abs(x - dx) > 0.3 || Math.abs(y - dy) > 0.3) pending = true;
else {
dx = x;
dy = y;
}
}
const rx = Math.round(dx);
const ry = Math.round(dy);
ov.style.opacity = shown ? "1" : "0";
if (pinBtn) {
pinBtn.style.pointerEvents = shown ? "" : "none";
pinBtn.tabIndex = shown ? 0 : -1;
}
ov.style.setProperty("--cx", rx + "px");
ov.style.setProperty("--cy", ry + "px");
ov.style.setProperty("--p", pressed ? "1" : "0");
ov.style.setProperty("--s", spot >= 0 ? "1" : "0");
// readout chip
put(eX, pad(rx));
put(eY, pad(ry));
const a = c.measure ? anchor : null;
let rowBText = "";
if (hit) rowBText = spot >= 0 ? "SNAP " + (SPOTS[spot]?.[2] ?? "") + " · " + hit.name : hit.name;
let rowCText = "";
let dist = 0;
if (a) {
dist = Math.hypot(rx - a[0], ry - a[1]);
rowCText = "Δ " + sgn(rx - a[0]) + " " + sgn(ry - a[1]) + " · L " + dist.toFixed(1);
}
if (rowB) {
put(rowB, rowBText);
rowB.hidden = !rowBText;
}
if (rowC) {
put(rowC, rowCText);
rowC.hidden = !rowCText;
}
if (chip) {
const cl = rx + 16 + chipW > W - 4 ? rx - 16 - chipW : rx + 16;
const ct = ry + 16 + chipH > H - 4 ? ry - 16 - chipH : ry + 16;
chip.style.left = Math.max(2, cl) + "px";
chip.style.top = Math.max(2, ct) + "px";
}
// edge tags
if (xTag) {
put(xTag, pad(rx));
xTag.style.left = clamp(rx, 16, Math.max(16, W - 16)) + "px";
}
if (yTag) {
put(yTag, pad(ry));
yTag.style.top = clamp(ry, 14, Math.max(14, H - 14)) + "px";
}
put(size, Math.round(W) + " × " + Math.round(H));
// snap box and dimensions
if (bx) {
if (hit) {
bx.style.opacity = "1";
bx.style.left = hit.l + "px";
bx.style.top = hit.t + "px";
bx.style.width = hit.r - hit.l + "px";
bx.style.height = hit.b - hit.t + "px";
put(bw, String(Math.round(hit.r - hit.l)));
put(bh, String(Math.round(hit.b - hit.t)));
put(bn, hit.name);
if (bw) bw.style.top = hit.t < 26 ? "3px" : "-19px";
if (bh) {
const inside = hit.l < 42;
bh.style.left = inside ? "3px" : "auto";
bh.style.right = inside ? "auto" : "calc(100% + 4px)";
}
if (bn) {
const inside = hit.b > H - 28;
bn.style.top = inside ? "auto" : "calc(100% + 4px)";
bn.style.bottom = inside ? "3px" : "auto";
}
} else bx.style.opacity = "0";
}
// measuring line
if (svg && sLine && sMark) {
svg.style.display = a ? "block" : "none";
if (a) {
sLine.setAttribute("x1", String(a[0]));
sLine.setAttribute("y1", String(a[1]));
sLine.setAttribute("x2", String(rx));
sLine.setAttribute("y2", String(ry));
sMark.setAttribute("x", String(a[0] - 3.5));
sMark.setAttribute("y", String(a[1] - 3.5));
}
}
if (mid) {
mid.hidden = !a || dist < 28;
if (a) {
mid.style.left = (a[0] + rx) / 2 + "px";
mid.style.top = (a[1] + ry) / 2 + "px";
put(mid, dist.toFixed(1));
}
}
if (pinBtn) {
const t = a ? "Clear" : "Pin";
put(pinBtn, t);
pinBtn.setAttribute("aria-pressed", a ? "true" : "false");
pinBtn.setAttribute("aria-label", a ? "Clear the measurement origin" : "Pin a measurement origin at the crosshair");
}
if (say && live) {
say = false;
let s = "X " + rx + ", Y " + ry;
if (hit) s += spot >= 0 ? ", snapped to " + hit.name + " " + (SPOTS[spot]?.[3] ?? "") : ", over " + hit.name;
if (a) s += ". Offset " + sgn(rx - a[0]) + ", " + sgn(ry - a[1]) + ", distance " + dist.toFixed(1);
live.textContent = s;
}
return pending;
};
const frame = (ts: number) => {
handle = 0;
if (!visible || docHidden) return;
const dt = lastT ? Math.min(0.05, (ts - lastT) / 1000) : 0.016;
lastT = ts;
measureBoxes();
const eng = engagedAt(ts);
let moving = false;
if (!eng) {
if (wasEngaged || idleSince < 0) {
idleSince = ts;
lastIdle = -1;
wasEngaged = false;
anchor = null;
kb = false;
}
const still = reduced || !cfg.current.idlePatrol;
const pts: Pt[] = [];
// patrol the biggest targets first; slivers would drown in their own tags
const big = boxes.filter((b) => b.r - b.l >= 40 && b.b - b.t >= 24);
const tour = (big.length ? big : boxes).slice().sort((a, b) => (b.r - b.l) * (b.b - b.t) - (a.r - a.l) * (a.b - a.t));
for (const b of tour) {
pts.push([b.l, b.t], [(b.l + b.r) / 2, (b.t + b.b) / 2]);
}
if (pts.length) {
let k = 1 % pts.length;
if (!still) {
const i = Math.floor((ts - idleSince) / DWELL);
k = i % pts.length;
if (i !== lastIdle) {
lastIdle = i;
const prev = pts[k - 1];
anchor = cfg.current.measure && k % 2 === 1 && prev ? prev : null;
}
}
const p = pts[k];
if (p) {
tx = clamp(p[0], 0, W);
ty = clamp(p[1], 0, H);
}
} else if (still) {
tx = W * 0.62;
ty = H * 0.44;
} else {
tx = W * (0.5 + 0.26 * Math.sin(ts * 0.0004));
ty = H * (0.5 + 0.2 * Math.sin(ts * 0.00053 + 1.3));
}
if (px < 0 || still) {
px = tx;
py = ty;
} else {
const k = 1 - Math.exp(-dt * 6.5);
px += (tx - px) * k;
py += (ty - py) * k;
}
} else {
wasEngaged = true;
if (px < 0 || reduced) {
px = tx;
py = ty;
} else if (settle > 0) {
px += (tx - px) * 0.42;
py += (ty - py) * 0.42;
settle--;
if (Math.abs(tx - px) < 0.3 && Math.abs(ty - py) < 0.3) settle = 0;
moving = settle > 0;
} else {
px = tx;
py = ty;
}
}
shown = eng || cfg.current.idlePatrol;
const pending = draw();
if (visible && !docHidden && ((!reduced && (eng || cfg.current.idlePatrol)) || moving || pending)) handle = requestAnimationFrame(frame);
};
const kick = () => {
if (!handle && visible && !docHidden) handle = requestAnimationFrame(frame);
};
redraw.current = kick;
// wake: remember where the crosshair is so it eases to the pointer
const wake = (ts: number) => {
if (!engagedAt(ts)) {
settle = 7;
tx = px;
ty = py;
}
};
const inPin = (e: Event) => !!pinBtn && pinBtn.contains(e.target as Node);
const local = (e: PointerEvent): Pt => {
const rr = root.getBoundingClientRect();
const w = root.offsetWidth;
const h = root.offsetHeight;
return [
clamp((e.clientX - rr.left) * (rr.width > 0 ? w / rr.width : 1), 0, w),
clamp((e.clientY - rr.top) * (rr.height > 0 ? h / rr.height : 1), 0, h),
];
};
const aim = (e: PointerEvent) => {
const p = local(e);
tx = clamp(p[0] + (grab ? grab[0] : 0), 0, root.offsetWidth);
ty = clamp(p[1] + (grab ? grab[1] : 0), 0, root.offsetHeight);
lastInput = e.timeStamp;
kb = false;
kick();
};
const toggleAnchor = () => {
anchor = anchor ? null : [Math.round(dx), Math.round(dy)];
say = true;
kick();
};
const onEnter = (e: PointerEvent) => {
if (e.pointerType === "touch" || inPin(e)) return;
wake(e.timeStamp);
hovering = true;
pType = e.pointerType;
aim(e);
};
const onMove = (e: PointerEvent) => {
if (inPin(e)) return;
if (e.pointerType === "touch" && !dragging) return;
if (e.pointerType !== "touch" && !hovering) wake(e.timeStamp);
if (e.pointerType !== "touch") hovering = true;
pType = e.pointerType;
aim(e);
};
const onDown = (e: PointerEvent) => {
if (inPin(e) || (e.pointerType === "mouse" && e.button !== 0)) return;
wake(e.timeStamp);
pType = e.pointerType;
pressed = true;
if (e.pointerType === "touch") dragging = true;
else hovering = true;
if (grip && grip.contains(e.target as Node)) {
const p = local(e);
grab = [dx - p[0], dy - p[1]];
try {
grip.setPointerCapture(e.pointerId);
} catch {
/* capture is a nicety */
}
} else grab = null;
aim(e);
};
const onUp = (e: PointerEvent) => {
pressed = false;
dragging = false;
grab = null;
lastInput = e.timeStamp;
kick();
};
const onLeave = (e: PointerEvent) => {
if (e.pointerType === "touch" || inPin(e)) return;
hovering = false;
pressed = false;
lastInput = -Infinity;
kick();
};
const onClick = (e: MouseEvent) => {
if (!cfg.current.measure || pType === "touch" || inPin(e)) return;
const t = e.target as Element | null;
if (t && t.closest && t.closest(INTERACTIVE)) return;
toggleAnchor();
};
const onPin = (e: Event) => {
e.stopPropagation();
toggleAnchor();
};
const onFocus = (e: FocusEvent) => {
let vis = false;
try {
vis = root.matches(":focus-visible");
} catch {
vis = false;
}
if (!vis) return;
wake(e.timeStamp);
focused = true;
kb = true;
if (dx >= 0) {
tx = dx;
ty = dy;
px = dx;
py = dy;
}
lastInput = e.timeStamp;
say = true;
kick();
};
const onBlur = () => {
focused = false;
kb = false;
kick();
};
const onKey = (e: KeyboardEvent) => {
if (e.target !== root || e.ctrlKey || e.metaKey || e.altKey) return;
const k = e.key;
const step = e.shiftKey ? 10 : 1;
const nudge = (ax: number, ay: number) => {
wake(e.timeStamp);
const bx0 = dx >= 0 ? dx : px;
const by0 = dy >= 0 ? dy : py;
tx = clamp(bx0 + ax * step, 0, root.offsetWidth);
ty = clamp(by0 + ay * step, 0, root.offsetHeight);
px = tx;
py = ty;
};
let handled = true;
if (k === "ArrowLeft") nudge(-1, 0);
else if (k === "ArrowRight") nudge(1, 0);
else if (k === "ArrowUp") nudge(0, -1);
else if (k === "ArrowDown") nudge(0, 1);
else if (k === "Home") {
wake(e.timeStamp);
tx = root.offsetWidth / 2;
ty = root.offsetHeight / 2;
px = tx;
py = ty;
} else if ((k === "n" || k === "N" || k === "p" || k === "P")) {
measureBoxes();
if (boxes.length) {
wake(e.timeStamp);
const dir = k === "n" || k === "N" ? 1 : -1;
kbIdx = (kbIdx + dir + boxes.length) % boxes.length;
const b = boxes[kbIdx];
if (b) {
tx = (b.l + b.r) / 2;
ty = (b.t + b.b) / 2;
px = tx;
py = ty;
}
} else handled = false;
} else if (k === "Enter" || k === " ") {
if (cfg.current.measure) toggleAnchor();
else handled = false;
} else if (k === "Escape") {
if (anchor) anchor = null;
else handled = false;
} else handled = false;
if (!handled) return;
e.preventDefault();
kb = true;
lastInput = e.timeStamp;
say = true;
kick();
};
const onMq = () => {
reduced = mq.matches;
kick();
};
const onVis = () => {
docHidden = document.hidden;
if (!docHidden) kick();
};
const io = new IntersectionObserver(
(en) => {
visible = !!en[0]?.isIntersecting;
if (visible) kick();
},
{ rootMargin: "200px" },
);
const ro = new ResizeObserver(() => kick());
root.addEventListener("pointerenter", onEnter);
root.addEventListener("pointermove", onMove);
root.addEventListener("pointerdown", onDown);
root.addEventListener("pointerup", onUp);
root.addEventListener("pointercancel", onUp);
root.addEventListener("pointerleave", onLeave);
root.addEventListener("click", onClick);
root.addEventListener("focus", onFocus);
root.addEventListener("blur", onBlur);
root.addEventListener("keydown", onKey);
pinBtn?.addEventListener("click", onPin);
mq.addEventListener("change", onMq);
document.addEventListener("visibilitychange", onVis);
io.observe(root);
ro.observe(root);
kick();
return () => {
root.removeEventListener("pointerenter", onEnter);
root.removeEventListener("pointermove", onMove);
root.removeEventListener("pointerdown", onDown);
root.removeEventListener("pointerup", onUp);
root.removeEventListener("pointercancel", onUp);
root.removeEventListener("pointerleave", onLeave);
root.removeEventListener("click", onClick);
root.removeEventListener("focus", onFocus);
root.removeEventListener("blur", onBlur);
root.removeEventListener("keydown", onKey);
pinBtn?.removeEventListener("click", onPin);
mq.removeEventListener("change", onMq);
document.removeEventListener("visibilitychange", onVis);
io.disconnect();
ro.disconnect();
if (handle) cancelAnimationFrame(handle);
handle = 0;
redraw.current = () => {};
};
}, []);
const guide = "rgba(255,255,255,0.62)";
const maskX = "linear-gradient(90deg, rgba(0,0,0,0.22), #000 var(--cx), rgba(0,0,0,0.22))";
const maskY = "linear-gradient(180deg, rgba(0,0,0,0.22), #000 var(--cy), rgba(0,0,0,0.22))";
const ease = "cubic-bezier(0.2, 0.8, 0.2, 1)";
return (
<div
ref={rootRef}
role="group"
aria-label="Crosshair measuring surface"
aria-describedby={descId}
tabIndex={0}
className={
"relative min-h-[12rem] cursor-none touch-pan-y overflow-hidden focus-visible:!rounded-[inherit] focus-visible:!outline-2 focus-visible:!outline-offset-[-2px] focus-visible:!outline-[#60a5fa]" +
(className ? " " + className : "")
}
>
{children}
<span id={descId} className="sr-only">
Arrow keys move the crosshair by one pixel, Shift by ten. N and P jump between snap targets. Enter pins a
measurement origin, Escape clears it, Home centres the crosshair.
</span>
<span data-ccr-live="" role="status" aria-live="polite" className="sr-only" />
<div
ref={ovRef}
aria-hidden
className="pointer-events-none absolute inset-0 overflow-hidden motion-reduce:!transition-none"
style={{ opacity: idlePatrol ? 1 : 0, transition: "opacity .22s ease", "--cx": "62%", "--cy": "44%", "--p": "0", "--s": "0", "--ccr-a": acc } as CSSProperties}
>
{/* guides: bright at the crosshair, fading toward the edges; difference blend reads on any ground */}
<div
className="absolute inset-x-0"
style={{
top: "calc(var(--cy) - 0.5px)",
height: 0,
borderTop: "1px " + line + " " + guide,
mixBlendMode: "difference",
maskImage: maskX,
WebkitMaskImage: maskX,
}}
/>
<div
className="absolute inset-y-0"
style={{
left: "calc(var(--cx) - 0.5px)",
width: 0,
borderLeft: "1px " + line + " " + guide,
mixBlendMode: "difference",
maskImage: maskY,
WebkitMaskImage: maskY,
}}
/>
{/* measuring line from the pinned origin */}
<svg width="100%" height="100%" className="absolute inset-0 overflow-visible" style={{ display: "none" }}>
<line stroke={acc} strokeWidth="1.25" strokeDasharray="4 3" />
<rect width="7" height="7" fill="#0a0b0e" stroke={acc} strokeWidth="1.25" />
</svg>
<div
data-ccr="mid"
hidden
style={{ ...TAG, transform: "translate(-50%, -50%)", color: acc, boxShadow: "inset 0 0 0 1px " + acc }}
/>
{/* snap box with dimension tags */}
<div
data-ccr="box"
className="absolute motion-reduce:!transition-none"
style={{
opacity: 0,
left: 0,
top: 0,
width: 0,
height: 0,
boxShadow: "inset 0 0 0 1px " + acc,
background: "color-mix(in srgb, " + acc + " 9%, transparent)",
transition: "left .12s " + ease + ", top .12s " + ease + ", width .12s " + ease + ", height .12s " + ease + ", opacity .12s",
}}
>
<span data-ccr="bw" style={{ ...DIM, left: "50%", transform: "translateX(-50%)" }}>
0
</span>
<span data-ccr="bh" style={{ ...DIM, top: "50%", transform: "translateY(-50%)" }}>
0
</span>
<span data-ccr="bn" style={{ ...DIM, left: 0, background: "#0a0b0e", boxShadow: "inset 0 0 0 1px " + acc }} />
</div>
{/* rulers */}
<div
className="absolute inset-x-0 top-0"
style={{
display: showRulers ? "block" : "none",
height: 14,
background:
"linear-gradient(90deg, rgba(255,255,255,0.6) 1px, transparent 1px) 0 100% / 50px 8px repeat-x, linear-gradient(90deg, rgba(255,255,255,0.3) 1px, transparent 1px) 0 100% / 10px 4px repeat-x, rgba(8,9,11,0.58)",
boxShadow: "inset 0 -1px 0 rgba(255,255,255,0.14)",
}}
>
<span data-ccr="xtag" style={{ ...TAG, top: 0, transform: "translateX(-50%)", left: "var(--cx)" }}>
000
</span>
</div>
<div
className="absolute inset-y-0 left-0"
style={{
display: showRulers ? "block" : "none",
width: 14,
background:
"linear-gradient(180deg, rgba(255,255,255,0.6) 1px, transparent 1px) 100% 0 / 8px 50px repeat-y, linear-gradient(180deg, rgba(255,255,255,0.3) 1px, transparent 1px) 100% 0 / 4px 10px repeat-y, rgba(8,9,11,0.58)",
boxShadow: "inset -1px 0 0 rgba(255,255,255,0.14)",
}}
>
<span data-ccr="ytag" style={{ ...TAG, left: 0, transform: "translateY(-50%)", top: "var(--cy)", padding: "0 2px", fontSize: 9 }}>
000
</span>
</div>
{/* reticle and dot */}
<div style={{ display: showRings ? "block" : "none" }}>
<div
className="absolute motion-reduce:!transition-none"
style={{
left: "var(--cx)",
top: "var(--cy)",
width: 52,
height: 52,
mixBlendMode: "difference",
backgroundImage:
"linear-gradient(#fff,#fff), linear-gradient(#fff,#fff), linear-gradient(#fff,#fff), linear-gradient(#fff,#fff)",
backgroundPosition: "50% 0, 50% 100%, 0 50%, 100% 50%",
backgroundSize: "1px 7px, 1px 7px, 7px 1px, 7px 1px",
backgroundRepeat: "no-repeat",
transform: "translate(-50%, -50%) scale(calc(1 - var(--p) * 0.2))",
opacity: "calc(1 - var(--s) * 0.85)",
transition: "transform .16s " + ease + ", opacity .14s",
}}
>
<span
className="absolute rounded-full"
style={{ inset: 6, boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.8)" }}
/>
</div>
<div
className="absolute motion-reduce:!transition-none"
style={{
left: "var(--cx)",
top: "var(--cy)",
width: 20,
height: 20,
boxShadow: "inset 0 0 0 1.5px " + acc,
transform: "translate(-50%, -50%) rotate(calc(45deg - var(--s) * 45deg)) scale(calc(0.7 + var(--s) * 0.3))",
opacity: "var(--s)",
transition: "transform .18s " + ease + ", opacity .14s",
}}
/>
<div
className="absolute rounded-full"
style={{
left: "var(--cx)",
top: "var(--cy)",
width: 6,
height: 6,
transform: "translate(-50%, -50%)",
background: acc,
boxShadow: "0 0 0 3px color-mix(in srgb, " + acc + " 28%, transparent), 0 0 14px " + acc,
}}
/>
{/* touch handle: drag the crosshair with a finger, offset kept so it stays visible */}
<div
data-ccr="grip"
className="absolute touch-none rounded-full [@media(pointer:coarse)]:pointer-events-auto"
style={{ left: "var(--cx)", top: "var(--cy)", width: 48, height: 48, transform: "translate(-50%, -50%)" }}
/>
</div>
{/* readout chip */}
<div
data-ccr="chip"
className="absolute motion-reduce:!transition-none"
style={{
display: showReadout ? "block" : "none",
left: 0,
top: 0,
font: "500 10px/1.5 " + MONO,
fontVariantNumeric: "tabular-nums",
color: "#fff",
background: "rgba(8,9,11,0.92)",
boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.16), 0 8px 24px -6px rgba(0,0,0,0.5)",
borderRadius: 6,
padding: "5px 8px",
whiteSpace: "nowrap",
}}
>
<div>
<span style={{ color: "rgba(255,255,255,0.5)" }}>X </span>
<span data-ccr="x">000</span>
<span style={{ color: "rgba(255,255,255,0.5)" }}> {"·"} Y </span>
<span data-ccr="y">000</span>
</div>
<div data-ccr="rb" hidden style={{ color: "rgba(255,255,255,0.66)" }} />
<div data-ccr="rc" hidden style={{ color: "#93c5fd" }} />
</div>
{/* status strip */}
<div
className="absolute bottom-2 left-[22px] right-2 flex items-end justify-between"
style={{ display: showReadout ? "flex" : "none" }}
>
<span data-ccr="size" style={{ ...PILL, letterSpacing: "0.06em", padding: "5px 8px", color: "rgba(255,255,255,0.6)" }}>
0 {"×"} 0
</span>
<button
type="button"
data-ccr="pin"
className="pointer-events-auto cursor-pointer transition-colors hover:!bg-white/15 focus-visible:!rounded-full focus-visible:!outline-2 focus-visible:!outline-offset-2 focus-visible:!outline-[#60a5fa] motion-reduce:transition-none [@media(pointer:coarse)]:!h-10 [@media(pointer:coarse)]:!px-5"
style={{ ...PILL, display: measure ? "inline-block" : "none", border: 0 }}
>
Pin
</button>
</div>
</div>
</div>
);
}
Free. Pro unlocks everything, from $27/month.
Details
About this component
The cursor stays deliberately instant, because the effect is borrowed from CAD and design tools where lag would be a defect. Two hairline guides cross at the pointer, bright at the crosshair and fading toward the edges, drawn with a difference blend so they read on a dark or a light surface. A chip beside the pointer shows X and Y in tabular mono, and the same numbers ride tags on ticked rulers along the top and left edges. Mark elements with data-snap (or pass snapSelector) and the crosshair snaps to their corners, edge midpoints and centre inside a 14px radius: the reticle turns into an accent square, the element is outlined and its width and height are tagged on its edges. With measure on (off by default), click, press Enter, or use the Pin button to drop an origin, and the chip adds the offset and straight-line distance with a dashed measuring line. With idlePatrol on (off by default) and no pointer present it patrols the snap targets by itself, pausing on corners and centres and measuring between them, so a static card still shows live readouts; without it the overlay stays hidden until the pointer or focus arrives, like a plain cursor effect. Position writes straight to CSS variables and text nodes from one rAF loop that sleeps off-screen, so moving the mouse never re-renders React. Touch: drag the ring (it keeps your finger offset) or tap to place it, and use Pin to measure.
Curator’s note
Crosshair Cursor Readout, rebuilt in October. The first version drew two guides and a coordinate chip; this one is a small measuring tool, with rulers, snapping, dimensions and pinned distances, still with no lag and no dependencies.


