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.
Installation
CLI (Recommended)
npx shadcn@latest add https://satisui.xyz/r/flipping-word-swap.jsonManual
- Install the following dependencies:
npm install gsap @gsap/reactyarn add gsap @gsap/reactpnpm add gsap @gsap/react- 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
| Prop | Type | Default | Description |
|---|---|---|---|
word1 | string | Required | The initial word to display. |
word2 | string | Required | The word to reveal on hover. |
className | string | - | Optional additional class names for the container. |