Text Fall Button

PreviousNext

A button with dynamic text that falls into place with a smooth, elastic motion on hover or click.

Docs
paceuiui

Preview

Loading preview…
gsap/text-fall-button.tsx
"use client";

import { ComponentProps, MouseEvent, useEffect, useRef } from "react";

import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { SplitText } from "gsap/SplitText";

import { cn } from "@/lib/utils";

gsap.registerPlugin(SplitText);

type TextFallButtonProps = {
    effectOnHover?: boolean;
    effectOnLoad?: boolean;
} & ComponentProps<"button">;

export const TextFallButton = ({
    className,
    children,
    effectOnHover = true,
    effectOnLoad = true,
    ...props
}: TextFallButtonProps) => {
    const buttonRef = useRef<HTMLButtonElement | null>(null);
    const textRef = useRef<HTMLSpanElement | null>(null);
    const splitTextRef = useRef<SplitText | null>(null);
    const timelineRef = useRef<gsap.core.Timeline | null>(null);

    const { contextSafe } = useGSAP();

    const triggerTextFallEffect = contextSafe(() => {
        const element = buttonRef.current;
        const splitText = splitTextRef.current;
        if (!element || !splitText) return;

        timelineRef.current?.kill();

        timelineRef.current = gsap.timeline();
        gsap.to(splitText.chars, {
            duration: 0,
            y: -60,
        });
        timelineRef.current
            .add("start")
            .to(element, {
                scale: 0.95,
                y: 4,
                duration: 0.1,
            })
            .to(element, {
                scale: 1,
                y: 0,
                duration: 0.2,
            })
            .to(
                splitText.chars,
                {
                    duration: 1,
                    y: 0,
                    stagger: 0.05,
                    ease: "elastic.out(0.75, 0.25)",
                },
                "start",
            );
    });

    useEffect(() => {
        if (textRef.current)
            splitTextRef.current = new SplitText(textRef.current, {
                type: "chars",
            });
        if (effectOnLoad) {
            triggerTextFallEffect();
        }
        return () => {
            splitTextRef.current?.revert();
            splitTextRef.current = null;
        };
    }, [effectOnLoad, triggerTextFallEffect]);

    const onClick = (e: MouseEvent<HTMLButtonElement>) => {
        triggerTextFallEffect();
        props.onClick?.(e);
    };

    return (
        <button
            {...props}
            ref={buttonRef}
            onMouseEnter={() => effectOnHover && triggerTextFallEffect()}
            onClick={onClick}
            className={cn("", className)}>
            <span ref={textRef} className="absolute">
                {children}
            </span>
            <span className="opacity-0">{children}</span>
        </button>
    );
};

Installation

npx shadcn@latest add @paceui/text-fall-button

Usage

import { TextFallButton } from "@/components/ui/text-fall-button"
<TextFallButton />