Skip to content

cursor

A floating element that follows the mouse with gsap.quickTo, hidden until the cursor enters a registered trigger. Each trigger declares the content it wants the cursor to display — text, title, image, anything matching aa-cursor-{part} — and the library swaps it in on mouseenter, then plays a small fall-and-rotate exit on mouseleave.

You can have as many cursors on a page as you want. aa-cursor="<name>" pairs with aa-cursor-trigger="<name>" — same name, same cursor. Both empty (aa-cursor and aa-cursor-trigger with no value) is the default unnamed pair.

Skipped on touch-only devices via (hover: hover) so the cursor element stays hidden and no listeners are attached on iOS / Android.

<!-- Default unnamed cursor; pairs with `aa-cursor-trigger` (no value). -->
<div aa-cursor>
<div>
<span aa-cursor-title></span>
<span aa-cursor-text></span>
</div>
</div>
<!-- A second, named cursor (e.g. for image previews); pairs with `aa-cursor-trigger="preview"`. -->
<div aa-cursor="preview">
<div>
<img aa-cursor-image alt="" />
</div>
</div>
<!-- Triggers select their cursor by name. -->
<a href="#" aa-cursor-trigger aa-cursor-title="External" aa-cursor-text="Visit site">Link</a>
<a href="#" aa-cursor-trigger="preview" aa-cursor-image="/portrait.jpg">Profile</a>

The first nested <div> of each cursor element is the show/hide subject — the script tweens its opacity, y, and rotation. The outer [aa-cursor] element only handles position + opacity, so a Webflow-style display: none on the outer element is fine while authoring.

AttributeWhat it marks
aa-cursorCursor wrapper. Optional value names the cursor so triggers can target it; multiple cursors with the same name are tolerated but only the first wins (warns in debug).
aa-cursor-triggerTrigger element. The value selects which [aa-cursor] to use; matched by name (both empty = default pair). Triggers whose name has no matching cursor are silently ignored.
aa-cursor-{part} (inside cursor)Slot. Holds the element whose content gets replaced.
aa-cursor-{part} (on each trigger)Value to inject into the matching slot on hover.

{part} is open-ended: any name works. image (or any <img> slot) is treated as src instead of textContent. Everything else gets textContent.

gsap.

  • Pins the cursor element to position: fixed; left: 0; top: 0 and follows the mouse via gsap.quickTo.
  • Adjusts cursor offset near the right edge (flips to xPercent: -100) and bottom edge (yPercent: -120) so the cursor never clips off-screen.
  • Show: opacity 0 → 1 on the wrapper, opacity 0 → 1 on the inner.
  • Hide: opacity → 0 on both, plus a small downward fall + random rotation on the inner so it feels physical.
  • If the cursor is mid-hide when a new trigger is hovered, the new trigger is queued and shown after the hide tween finishes (only if the cursor is still over it).

Hover any tile to swap the cursor’s title and text. Move out — it falls away.

A separate [aa-cursor="preview"] element with just an image slot. Triggers below set aa-cursor-trigger="preview" (matching the cursor’s name) and aa-cursor-image=.... Both cursors coexist on the same page — they share one mousemove listener and update independently.