GET THE INITIAL PROMPT:
Build a single interactive button as a self-contained block I can drop into an Elementor page. Recreate the Cuberto-style button, which combines THREE effects at once: (1) a magnetic pull, (2) a circular fill that grows from the cursor’s entry point on hover, and (3) a vertical text swap. Use GSAP 3 for all animation. Only fall back to vanilla JS (requestAnimationFrame + CSS transitions) if GSAP cannot be loaded in this environment.
DELIVERY FORMAT
- Output one HTML widget containing: the markup, a <style> block with all CSS scoped under a unique class (use .cbx-btn as the namespace so nothing leaks into the rest of the page), and a <script> block.
- Ensure GSAP 3 is available. If Elementor isn’t already enqueuing it, load it from CDN (https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js) before the script runs, and only initialize after it has loaded.
- Everything must be self-contained and re-runnable (guard against double-initialization).
MARKUP <a class=”cbx-btn” href=”#”> <span class=”cbx-btn__fill” aria-hidden=”true”></span> <span class=”cbx-btn__label”> <span class=”cbx-btn__text”>Tell us</span> <span class=”cbx-btn__text cbx-btn__text–clone” aria-hidden=”true”>Tell us</span> </span> </a>
CSS REQUIREMENTS
- .cbx-btn: inline-flex, relative position, overflow:hidden (so the fill is clipped to the shape), pill shape (border-radius: 999px), generous padding (~18px 40px), 1px solid border, transparent background by default, will-change: transform, cursor:pointer, no text-decoration.
- .cbx-btn__fill: absolute, border-radius:50%, transform: scale(0), transform-origin: center, pointer-events:none, z-index 0. Its left/top are set by JS to the cursor’s entry point. Size it large enough that scale(1) fully covers the button from any entry point (e.g. width/height = 250% of the button’s largest dimension).
- .cbx-btn__label: relative, z-index 1, display:block, overflow:hidden (this clips the text swap).
- .cbx-btn__text: display:block. The clone is absolutely positioned over the original and starts at translateY(100%).
- Define color variables so they’re easy to change: default text color, fill color, and the text color shown while the fill is active (must contrast with the fill).
BEHAVIOR 1 — MAGNETIC PULL (on pointermove over the button)
- Get the button’s bounding rect. Compute cursor offset from the button’s CENTER: dx = clientX – (rect.left + rect.width/2), dy = clientY – (rect.top + rect.height/2).
- Move the button: gsap.to(btn, { x: dx * 0.35, y: dy * 0.35, duration: 0.6, ease: “power3.out” }).
- Move the label slightly more for depth: gsap.to(label, { x: dx * 0.55, y: dy * 0.55, duration: 0.6, ease: “power3.out” }).
- On pointerleave, spring both back to 0: gsap.to([btn, label], { x: 0, y: 0, duration: 0.7, ease: “elastic.out(1, 0.4)” }).
BEHAVIOR 2 — CIRCLE FILL (on pointerenter / pointerleave)
- On pointerenter: read the cursor’s position relative to the button, set the fill element’s left/top to that point (centered on it), then gsap.fromTo(fill, { scale: 0 }, { scale: 1, duration: 0.5, ease: “power2.out” }). Simultaneously animate the text color to the active color.
- On pointerleave: reposition the fill to the cursor’s EXIT point, then gsap.to(fill, { scale: 0, duration: 0.45, ease: “power2.in” }), and animate the text color back.
BEHAVIOR 3 — TEXT SWAP (tie into the same enter/leave)
- On enter: gsap.to(originalText, { yPercent: -100, duration: 0.4, ease: “power2.out” }) and gsap.to(cloneText, { yPercent: 0, duration: 0.4, ease: “power2.out” }) (clone starts at yPercent:100).
- On leave: reverse both back to their resting positions with the same duration/ease.
ACCESSIBILITY & TOUCH
- Keep it a real, focusable <a>. Add a clear :focus-visible outline.
- If window.matchMedia(“(hover: none)”).matches OR “(prefers-reduced-motion: reduce)” matches: skip the magnetic and circle-fill effects entirely and use a simple instant color change on hover/focus instead.
EXPOSE THESE AS EASY-TO-EDIT VALUES AT THE TOP
- Button text, default text color, fill color, active text color, border color, magnetic strength multiplier (default 0.35 / 0.55).
Keep the motion subtle and premium — this should feel like the real Cuberto button, not exaggerated.