Hover Button Tutorial

In this tutorial, you will build an animated button with GSAP, SplitText, and an optional loading overlay. The final result matches the effect used in your "HoverButton" component.

1) Setup & Imports

First, install GSAP, the React plugin, and Lucide icons. Then register "SplitText" so you can animate both text lines cleanly against each other.

1"use client"
2import React, { useEffect, useId } from "react"
3import gsap from "gsap"
4import { SplitText } from "gsap/all"
5import { useGSAP } from "@gsap/react"
6import { Loader2 } from "lucide-react"
7
8gsap.registerPlugin(SplitText)

2) Button Structure

The button consists of a wrapper, an SVG layer for the morph effect, two text spans, and a loader layer. "text1" is the default label, while "text2" slides in from below on hover.

1return (
2  <div
3    id={id}
4    onMouseEnter={() => !disabled && !loading && handleEnter()}
5    onMouseLeave={() => handleLeave()}
6    className="relative flex items-center justify-center w-full overflow-hidden px-4 py-2 rounded-full"
7  >
8    <svg className="svg absolute inset-0 w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
9      <rect width="300" x="-100" y={200} height="100" />
10      <path d={initialPath} />
11    </svg>
12
13    <span className="text1 relative font-medium">{text1}</span>
14    <span className="text2 absolute font-medium">{text2}</span>
15
16    <div className={`loader-cover absolute inset-0 ${showLoading ? "flex" : "hidden"}`}>
17      <div className="loader-wrapper">
18        <Loader2 className="animate-spin" />
19      </div>
20    </div>
21  </div>
22)

3) Hover Animation with SplitText

On enter, create "SplitText" instances for both lines, scale the button up slightly, and morph the SVG shape. On leave, reset everything cleanly and run the animation in reverse.

1function handleEnter() {
2  killAllTweensAndResetSplits()
3  text1Ref.current = new SplitText(`#${id} .text1`, { type: "lines", mask: "lines" })
4  text2Ref.current = new SplitText(`#${id} .text2`, { type: "lines", mask: "lines" })
5
6  gsap.set(`#${id} .text2`, { display: "block", yPercent: 0 })
7  gsap.set(text2Ref.current.lines, { yPercent: 100 })
8
9  gsap.to(`#${id}`, { scale: 1.06, duration: 0.3, ease: "power2.out" })
10  gsap.to(`#${id} .svg path`, { duration: 0.3, ease: "circ.out", attr: { d: hoverPath } })
11  gsap.to(`#${id} .svg rect`, { duration: 0.3, ease: "circ.out", attr: { y: "0" } })
12
13  gsap.to(text1Ref.current.lines, { yPercent: -100, duration: 0.5, ease: "power2.out", stagger: 0.05 })
14  gsap.to(text2Ref.current.lines, { yPercent: 0, duration: 0.5, ease: "power2.out", stagger: 0.05 })
15}
16
17function handleLeave() {
18  killAllTweensAndResetSplits()
19
20  text1Ref.current = new SplitText(`#${id} .text1`, { type: "lines", mask: "lines" })
21  text2Ref.current = new SplitText(`#${id} .text2`, { type: "lines", mask: "lines" })
22
23  gsap.to(`#${id}`, { scale: 1, duration: 0.3, ease: "power2.out" })
24  gsap.to(`#${id} .svg path`, { duration: 0.5, ease: "circ.out", attr: { d: initialPath } })
25  gsap.to(`#${id} .svg rect`, { duration: 0.5, ease: "circ.out", attr: { y: "200" } })
26
27  gsap.to(text1Ref.current.lines, { yPercent: 0, duration: 0.5, ease: "power2.out" })
28  gsap.to(text2Ref.current.lines, { yPercent: 100, duration: 0.3, ease: "power4.out" })
29}

4) Usage

Import the component and use "text1", "text2", and optionally "loading", "disabled", and "className".

1import HoverButton from "@/lib/components/hoverButton"
2
3<HoverButton
4  text1="Get Started"
5  text2="Let's go"
6  className="max-w-56 h-14"
7/>

Live Demo

Get StartedLet's go