Label
Morphs into a pill reading the hovered element's data-cursor-label.
npx shadcn@latest add https://hoverui.fun/ r/ label.jsonAdds components/cursors/label.tsx. The CLI also installs the useCursor hook it runs on, once, into your hooks folder. The files are yours from there.
- Kind
- Labels
- Hook
- useCursor
- Dependencies
- None
- License
- MIT
How it behaves
A dot that morphs into a pill showing the hovered element's data-cursor-label.
Usage
import { LabelCursor } from "@/components/cursors/label";
export function Section() {
return (
<section>
<LabelCursor />
{/* this section now wears Label */}
</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
- Colors the dot and pill through currentColor, e.g. text-pink-500. The words switch to black or white to stay readable.
- speed
- How quickly the dot and pill catch up with the pointer; lower trails further behind. Default 20.
- data-cursor-label
- On any element: the words the pill shows while the pointer is over it.
- 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";
/** A dot that morphs into a pill showing the hovered element's `data-cursor-label`. */
export function LabelCursor({
className = "",
speed = 20,
}: {
className?: string;
/** How quickly the dot and pill catch up with the pointer; lower trails further behind. */
speed?: number;
}) {
const pos = useRef({ x: 0, y: 0 });
const ref = useCursor((p, el) => {
const c = pos.current;
const group = el.firstChild as HTMLElement;
const label = p.target?.closest<HTMLElement>("[data-cursor-label]")?.dataset.cursorLabel;
c.x += (p.x - c.x) * p.ease(speed);
c.y += (p.y - c.y) * p.ease(speed);
group.style.transform = `translate(${c.x}px, ${c.y}px)`;
// Keep the old text while the pill scales out.
if (label) group.querySelector("span")!.textContent = label;
group.toggleAttribute("data-open", !!label);
});
return (
<div ref={ref} aria-hidden className={`${cursorRoot} ${className}`}>
<div className="group absolute">
<div className="absolute size-3 -translate-1/2 rounded-full bg-current transition-[scale,opacity] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-opacity group-data-open:scale-50 group-data-open:opacity-0" />
{/* The hairline uses the text's contrast color, so the pill keeps its edge over its own color. */}
<div
className="absolute -translate-1/2 scale-90 whitespace-nowrap rounded-full bg-current px-3 py-1.5 opacity-0 transition-[scale,opacity] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-opacity group-data-open:scale-100 group-data-open:opacity-100"
style={{ boxShadow: "0 0 0 1px oklch(from currentColor calc((0.7 - l) * 1000) 0 0 / 0.35)" }}
>
{/* Black or white, whichever reads on the pill's currentColor; plain white where relative colors are unsupported. */}
<span className="text-xs font-medium text-white" style={{ color: "oklch(from currentColor calc((0.7 - l) * 1000) 0 0)" }} />
</div>
</div>
</div>
);
}
It runs on the useCursor hook, which has its own source and API.
More labels cursors
- TooltipA tooltip that rises over anything clickable, reading its label or text. No markup needed.
- PeekShows where a link goes before you click it: the path for links on your site, the host for others.
- ElementHo, for hover. Spells a control's name in periodic-table tiles; hold to excite the atom, let go for its flame color.
- CipherA terminal block cursor that spills code as it moves. Over a control its name decodes out of scrambled glyphs, letter by letter; a click scrambles it again.
- ThreadYour own words on a thread: a ring turning round the pointer that unspools along your path as you move and wraps a control in its name. A click scatters the letters.
Made a cursor of your own? Contributed cursors carry their maker's name on their page.
Contribute a cursor