Context Cursor Badge
A free custom cursor badge react effect that names whatever the pointer is over.
August digest
The rebuild, the tour, the release
Northwind's storefront made fast, a first look inside the new editor, and everything that landed in 4.2.
Also in the archive: 34 case studies and 61 release notes.
New entries every Tuesday
Customise
Code
<CursorContextBadge />Context Cursor Badge is a free custom cursor badge react effect for Next.js. A small badge follows a fine pointer and takes its label from the nearest ancestor carrying data-cursor-label, so the markup that already describes a target describes the badge too - there is no second list of labels to keep in step with the first.
It is supplementary and nothing more. The badge is aria-hidden, pointer-events-none and unfocusable, because whatever it names must already be in the link or button's own accessible name; a reader who never sees it loses no information and no affordance. The whole effect is gated inside one gsap.matchMedia query, so on a coarse pointer or with reduced motion no listener is registered and no badge is shown, and the operating system's cursor is never hidden or replaced. Positioning goes through GSAP quickTo setters against cached root bounds, so a pointermove reads neither the DOM nor the badge, and the flip that keeps it inside the root uses the fixed maxWidth as a constant rather than measuring anything.
Install
Add the @te registry to your components.json once:
{
"registries": {
"@te": "https://templateempire.io/r/{name}.json"
}
}Then install the component:
npx shadcn@latest add @te/cursor-context-badgeUsage
import { CursorContextBadge } from "@/components/cursor-context-badge";
export default function Page() {
return (
<CursorContextBadge>
<div className="grid gap-3 sm:grid-cols-2">
<a href="/case-study" data-cursor-label="Open case study">
Northwind rebuild
</a>
<a href="/changelog" data-cursor-label="Read the changelog">
Release 4.2
</a>
</div>
</CursorContextBadge>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | required | The bounded content. Any descendant carrying data-cursor-label becomes a target; anything else is root space. |
| defaultLabel | string | "Explore" | What the badge says over root space that carries no target. Only used when showOnRoot is enabled. |
| lag | number | 0.16 | How long the badge takes to catch up with the pointer, in seconds, clamped to 0.05-0.5. This is the drift-behind feel. |
| radius | number | 999 | Badge corner radius in CSS pixels, clamped to 0-999. |
| offset | number | 18 | How far the badge sits from the pointer in CSS pixels, clamped to 8-40. It flips to the other side near the root's right edge. |
| transitionDuration | number | 0.18 | Label enter and exit duration in seconds, clamped to 0-0.45. The badge's geometry does not change with it. |
| maxWidth | number | 112 | Badge maximum width in CSS pixels, clamped to 72-168. It is also the constant the edge flip is computed from, which is why labels are truncated rather than allowed to wrap. |
| showOnRoot | boolean | false | Whether root space with no target shows defaultLabel. Off by default, so the badge appears only where it has something specific to say. |
| className | string | undefined | Additional classes merged onto the bounded wrapper with cn(). |
Customisation
Context Cursor Badge declares three tokens of its own, each falling back to a global semantic token, so it retints from an ancestor without touching the component source. Geometry and timing come from props rather than tokens, because they are clamped: --context-badge-panel is the badge surface, --context-badge-text is its label colour, and --context-badge-ring is the focus accent on annotated targets.
:root {
--context-badge-panel: var(--primary);
--context-badge-text: var(--primary-foreground);
--context-badge-ring: var(--ring);
}
<CursorContextBadge radius={8} offset={28} maxWidth={168} showOnRoot>
{children}
</CursorContextBadge>Frequently asked questions
- Is Context Cursor Badge free for commercial use?
- Yes. Context Cursor Badge is MIT licensed, so you can use it in personal and commercial projects without attribution.
- Where does the label come from?
- From the nearest ancestor of whatever the pointer is over that carries a data-cursor-label attribute, resolved with closest() and required to be inside this component's own root - a badge naming something from a sibling component would be worse than no badge at all. The value is trimmed and bounded to 32 characters, because the badge has a fixed maximum width and the flip that keeps it inside the root uses that width as a constant: a label allowed to grow would either reflow under the pointer or escape the bounds the clamp thinks it is respecting. Root space with no target shows nothing unless showOnRoot is enabled.
- Does it work for touch and keyboard users?
- They do not get a badge, and that is the design rather than a gap. The effect is built inside one gsap.matchMedia query gated on a fine pointer and no reduced-motion preference, so outside it no pointer listener is registered, no observer is created and no badge is drawn. Nothing is lost, because the badge only ever repeats what is already in the underlying control's own text or accessible name - that is the rule the demo follows, and it is worth following in your own markup. Keyboard focus styles belong to the controls themselves and are never covered, and the operating system's cursor is never suppressed.