Cursor label
FreeA small pill follows the pointer over a labelled area and says what a click does there: Drag, Play, Open. On touch it becomes a label in the corner.
pnpm dlx shadcn@latest add https://caliperui.site/r/cursor-label.jsonWhen to use #
Use it for
- Galleries, film posters and case-study tiles where the action is not obvious.
- Large media areas with one clear gesture.
Skip it when
- Ordinary buttons and links; their own labels already say it.
- Anywhere the native pointer matters, such as text editing.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Decoration over a real name
- Every labelled element keeps its own accessible name. The pill is hidden from assistive technology and never carries information that is not also in the page.
- The pointer is hidden, by choice
- Over a labelled element the pill replaces the native pointer. Pass nativeCursor to keep both.
Styling
- Ink pill, grows on press
- It uses the primary button's ink and scales to 1.12 while pressed, which is the only feedback it gives.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/cursor-label.json
Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { CursorLabel } from "@/components/ui/cursor-label" export function CursorLabelDemo() { const tile = "relative grid h-36 place-items-center rounded-surface border border-border bg-elevated outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"; return ( <CursorLabel className="grid w-full max-w-xl gap-3 p-4 sm:grid-cols-3"> <a href="#gallery" data-cursor-label="Drag" aria-label="Gallery, drag to browse" className={tile}> <span className="relative h-12 w-[74px]"> <i className="absolute inset-0 -translate-y-2.5 scale-90 rounded-[10px] bg-muted opacity-60 shadow-[0_0_0_1px_var(--border-strong)]" /> <i className="absolute inset-0 -translate-y-1 scale-95 rounded-[10px] bg-muted opacity-80 shadow-[0_0_0_1px_var(--border-strong)]" /> <i className="absolute inset-0 rounded-[10px] bg-elevated shadow-[0_0_0_1px_var(--border-strong)]" /> </span> </a> <a href="#film" data-cursor-label="Play" aria-label="Product film, play" className={tile}> <span className="grid size-14 place-items-center rounded-full shadow-[inset_0_0_0_1.5px_var(--foreground)]"> <span className="ms-1 border-y-8 border-s-[13px] border-y-transparent border-s-foreground" /> </span> </a> <a href="#case" data-cursor-label="Open" aria-label="Case study, open" className={tile}> <span className="grid h-14 w-[86px] content-center gap-2 rounded-lg bg-muted p-3 shadow-[0_0_0_1px_var(--border-strong)]"> <i className="h-1.5 rounded-full bg-border-strong" /> <i className="h-1.5 w-2/3 rounded-full bg-border-strong" /> <i className="h-1.5 w-1/2 rounded-full bg-border" /> </span> </a> </CursorLabel> ); }
API reference #
CursorLabel
component
Also accepts the native props of its root element and a ref.
Accessibility #
It is built on native elements and keeps aria-hidden in step with state.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.