Radar
A scope sweeping round the pointer. Over a control, it sweeps faster and pings a contact on it.
npx shadcn@latest add https://hoverui.fun/ r/ radar.jsonAdds components/cursors/radar.tsx. The CLI also installs the useCursor hook it runs on, once, into your hooks folder. The files are yours from there.
Move through the preview, then hover the controls and press.Open this on a computer to move it around: it follows a mouse, not a finger.
- Kind
- Mechanical
- Hook
- useCursor
- Dependencies
- None
- License
- MIT
How it behaves
A radar scope sweeping round the pointer; over anything clickable it widens, sweeps faster and pings a contact on the control's centre.
Usage
import { RadarCursor } from "@/components/cursors/radar";
export function Section() {
return (
<section>
<RadarCursor />
{/* this section now wears Radar */}
</section>
);
}Put it inside <body> in your root layout to wear it on every page. Nested cursors take over inside their own element.
Props
- className
- Extra classes for the cursor's root. It draws in its own colors, set in the file; any outline or pointer dot it has follows currentColor, e.g. text-pink-500. Leave mix-blend-difference off: it would turn those colors to negatives.
- Hover targets
- a, button, input, select, textarea, label, [role=button], [data-cursor-label]
Source
"use client";
import { useRef } from "react";
import { cursorRoot, useCursor } from "@/hooks/use-cursor";
const GREEN = "rgb(34 197 94)";
/** A radar scope sweeping round the pointer; over anything clickable it widens, sweeps faster and pings a contact on the control's centre. */
export function RadarCursor({ className = "" }: { className?: string }) {
const s = useRef({ a: 0, size: 1 });
const ref = useCursor((p, el) => {
const st = s.current;
if (!p.reduced) st.a += p.dt * Math.PI * (p.hover ? 4 : 1.6);
st.size += ((p.down ? 0.8 : p.hover ? 1.4 : 1) - st.size) * p.ease(12);
const [scope, sweep, blip] = el.children as HTMLCollectionOf<HTMLElement>;
scope.style.transform = `translate(${p.x}px, ${p.y}px) scale(${st.size})`;
sweep.style.transform = `translate(${p.x}px, ${p.y}px) scale(${st.size}) rotate(${st.a}rad)`;
// The contact sits on the control's centre and pings once per turn of the sweep.
const box = p.hover?.getBoundingClientRect();
const phase = (st.a % (Math.PI * 2)) / (Math.PI * 2);
if (box) blip.style.transform = `translate(${box.left + box.width / 2}px, ${box.top + box.height / 2}px) scale(${0.4 + phase})`;
blip.style.opacity = box ? String(p.reduced ? 1 : 1 - phase) : "0";
});
return (
<div ref={ref} aria-hidden className={`${cursorRoot} ${className}`}>
<div className="absolute size-12 -translate-1/2 rounded-full border border-current/70" style={{ color: GREEN }}>
<div className="absolute inset-[30%] rounded-full border border-current/40" />
<div className="absolute top-1/2 left-0 h-px w-full bg-current/30" />
<div className="absolute top-0 left-1/2 h-full w-px bg-current/30" />
<div className="absolute top-1/2 left-1/2 size-1.5 -translate-1/2 rounded-full bg-current" />
</div>
<div className="absolute size-12 -translate-1/2 rounded-full bg-[conic-gradient(from_270deg,transparent,rgb(34_197_94/0.55)_90deg,transparent_90deg)]" />
<div className="absolute size-5 -translate-1/2 rounded-full border-2 opacity-0" style={{ borderColor: GREEN }} />
</div>
);
}
It runs on the useCursor hook, which has its own source and API.
More mechanical cursors
- GearA gear that rolls as you travel. Over anything clickable, a second gear meshes in.
- ReadoutA drafting crosshair with live coordinates. Over a control, it reads the tag and size.
- CompassThe needle swings to your heading, overshoots, and settles north. It spins over anything clickable.
- ClockKeeps your visitor's real time. The hands race forward over controls and wind back on press.
Made a cursor of your own? Contributed cursors carry their maker's name on their page.
Contribute a cursor