Text animations

Flipping Word Swap

A component that displays a word and flips it character-by-character to a second word on mouse hover.

Hero / Headline

A large, prominent example ideal for hero sections or main headlines. This demonstrates the component's maximum visual impact.

Develop

Installation

npx shadcn@latest add https://satisui.xyz/r/flipping-word-swap.json

Manual

  1. Install the following dependencies:
npm install gsap @gsap/react
yarn add gsap @gsap/react
pnpm add gsap @gsap/react
  1. Copy and paste the following code into your project.
'use client';

import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { cn } from '@/lib/utils';

/**
 * A component that displays a word and flips it character-by-character
 * to a second word on mouse hover.
 */
interface FlippingWordSwapProps {
  /** The initial word to display. */
  word1: string;
  /** The word to reveal on hover. */
  word2: string;
  /** Optional additional class names for the container. */
  className?: string;
}

export default function FlippingWordSwap({
  word1,
  word2,
  className,
}: FlippingWordSwapProps) {
  const containerRef = useRef<HTMLDivElement>(null);

  const renderChars = (text: string, extraClassName = '', initialStyles = {}) =>
    text.split('').map((char, index) => (
      <span
        key={index}
        className={cn('char inline-block', extraClassName)}
        style={initialStyles}
      >
        {char === ' ' ? '\u00A0' : char}
      </span>
    ));

  useGSAP(
    () => {
      const word1Chars = gsap.utils.toArray('.char-1', containerRef.current);
      const word2Chars = gsap.utils.toArray('.char-2', containerRef.current);

      if (word1Chars.length === 0 || word2Chars.length === 0) return;

      const tl = gsap.timeline({ paused: true });

      tl.to(word1Chars, {
        rotationX: 90,
        opacity: 0,
        transformOrigin: 'center top',
        stagger: 0.05,
        duration: 0.4,
        ease: 'power2.in',
      }).to(
        word2Chars,
        {
          rotationX: 0,
          opacity: 1,
          transformOrigin: 'center bottom',
          stagger: 0.05,
          duration: 0.4,
          ease: 'power2.out',
        },
        '<0.275'
      );

      const onMouseEnter = () => tl.play();
      const onMouseLeave = () => tl.reverse();

      const container = containerRef.current;
      container?.addEventListener('mouseenter', onMouseEnter);
      container?.addEventListener('mouseleave', onMouseLeave);

      return () => {
        container?.removeEventListener('mouseenter', onMouseEnter);
        container?.removeEventListener('mouseleave', onMouseLeave);
      };
    },
    { scope: containerRef, dependencies: [word1, word2] }
  );

  return (
    <div
      ref={containerRef}
      className={cn(
        'relative cursor-pointer text-9xl w-full font-bold [transform-style:preserve-3d] [perspective:800px]',
        className
      )}
      role='button'
      tabIndex={0}
      aria-live='polite'
      aria-label={`${word1}, on hover changes to ${word2}`}
    >
      <span className='word-1 inline-block' aria-hidden='true'>
        {renderChars(word1, 'char-1')}
      </span>

      <span className='word-2 absolute inset-0 inline-block' aria-hidden='true'>
        {renderChars(word2, 'char-2', {
          transform: 'rotateX(-90deg)',
          opacity: 0,
          transformOrigin: 'center bottom',
        })}
      </span>

      <span className='sr-only'>{word1}</span>
    </div>
  );
}

Usage

Import the component and use it in your application.

import FlippingWordSwap from '@/components/ui/flipping-word-swap';

export default function MyComponent() {
  return (
    <div className='flex flex-col items-center justify-center gap-12 p-8'>
      {/* Default usage */}
      <FlippingWordSwap word1='Create' word2='Innovate' />

      {/* Customized usage with different styling */}
      <p className='text-xl'>
        We help you{' '}
        <FlippingWordSwap
          word1='design'
          word2='build'
          className='inline-block text-2xl font-semibold text-blue-500'
        />{' '}
        amazing products.
      </p>
    </div>
  );
}

Props

PropTypeDefaultDescription
word1stringRequiredThe initial word to display.
word2stringRequiredThe word to reveal on hover.
classNamestring-Optional additional class names for the container.