Sora UI

Cursor Bubble

A floating label that follows the cursor and elastic-pops into view over interactive targets, powered by GSAP.

Made by Axyl

Move your cursor over the links below — a floating label pops in with the target's text.

Loading...

CursorBubble follows the pointer with gsap.quickTo (a lightweight per-frame tween, cheaper than re-running a full tween every mousemove), offset from the cursor so it reads like a little tag trailing behind it. Each CursorBubbleTarget reports its own label to the bubble on hover, which pops in with an elastic ease and collapses back to a dot-sized, rotated resting state on leave — the same feel as Truus.co's footer link cursor.

Installation

File Structure

cursor-bubble.tsx
use-prefers-reduced-motion.tsx

Usage

import {
  CursorBubble,
  CursorBubbleTarget,
} from '@/components/sora-ui/effects/cursor-bubble';

export default function Page() {
  return (
    <CursorBubble>
      <CursorBubbleTarget label="say hi">
        <a href="mailto:hello@example.com">hello@example.com</a>
      </CursorBubbleTarget>
    </CursorBubble>
  );
}

Props

PropTypeDefault
children?
ReactNode
-
className?
string
-
bubbleClassName?
string
-

CursorBubbleTarget

PropTypeDefault
label?
string
"click"
children?
ReactNode
-
className?
string
-

When to Use

Mount one CursorBubble near the root of a page (or layout) and wrap individual links, logos, or buttons in CursorBubbleTarget to give them a contextual hover hint — "say hi", "watch reel", "to home" — instead of relying on the browser's default cursor alone. It reads best on desktop marketing/portfolio pages with a handful of key links, not dense UI with many small targets.

Only one bubble should be mounted per page; multiple CursorBubble providers will each attach their own pointermove listener and fight over the same targets.

Accessibility

The bubble is aria-hidden and pointer-events: none — it never intercepts clicks or enters the tab order, and screen readers ignore it entirely. It only activates on pointer devices: on touch/coarse pointers it renders nothing, and when prefers-reduced-motion: reduce is set it also doesn't render, watched live via usePrefersReducedMotion so toggling the setting mid-session updates immediately.

Credits

Inspired by Truus.co. Reimplemented for GSAP and React.

Built by Axyl. A motion-first component registry for React.

Last updated: 7/9/2026