Docs
Popup Text
Alphabets Will PopUp One by One
Installation
Install dependencies
npm install @gsap/reactRun the following command
It will create a new file popup-text.tsx inside the components/mage-ui/text directory.
mkdir -p components/mage-ui/text && touch components/mage-ui/text/popup-text.tsxPaste the code
Open the newly created file and paste the following code:
import React, { useRef, useEffect, useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { SplitText as GSAPSplitText } from 'gsap/SplitText';
import { useGSAP } from '@gsap/react';
gsap.registerPlugin(ScrollTrigger, GSAPSplitText, useGSAP);
// ============= COMPONENT DEFINITION =============
export interface PopupTextProps {
text: string;
className?: string;
delay?: number;
duration?: number;
ease?: string;
splitType?: 'chars' | 'words' | 'lines' | 'words, chars';
from?: {
opacity?: number;
y?: number;
x?: number;
scale?: number;
rotation?: number;
};
to?: {
opacity?: number;
y?: number;
x?: number;
scale?: number;
rotation?: number;
};
threshold?: number;
rootMargin?: string;
tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';
textAlign?: 'left' | 'center' | 'right' | 'justify';
onLetterAnimationComplete?: () => void;
}
const PopupTextComponent = ({
text,
className = '',
delay = 100,
duration = 0.6,
ease = 'power3.out',
splitType = 'chars',
from = { opacity: 0, y: 40 },
to = { opacity: 1, y: 0 },
threshold = 0.1,
rootMargin = '-100px',
textAlign = 'center',
tag = 'p',
onLetterAnimationComplete
}: PopupTextProps) => {
const ref = useRef<HTMLElement>(null);
const animationCompletedRef = useRef(false);
const [fontsLoaded, setFontsLoaded] = useState<boolean>(false);
useEffect(() => {
if (document.fonts.status === 'loaded') {
setFontsLoaded(true);
} else {
document.fonts.ready.then(() => {
setFontsLoaded(true);
});
}
}, []);
useGSAP(
() => {
if (!ref.current || !text || !fontsLoaded) return;
const el = ref.current as HTMLElement & {
_rbsplitInstance?: any;
};
if (el._rbsplitInstance) {
try {
el._rbsplitInstance.revert();
} catch (_) { }
el._rbsplitInstance = undefined;
}
const startPct = (1 - threshold) * 100;
const marginMatch = /^(-?\d+(?:\.\d+)?)(px|em|rem|%)?$/.exec(rootMargin);
const marginValue = marginMatch ? parseFloat(marginMatch[1]) : 0;
const marginUnit = marginMatch ? marginMatch[2] || 'px' : 'px';
const sign =
marginValue === 0
? ''
: marginValue < 0
? `-=${Math.abs(marginValue)}${marginUnit}`
: `+=${marginValue}${marginUnit}`;
const start = `top ${startPct}%${sign}`;
let targets: Element[] = [];
const splitInstance = new GSAPSplitText(el, {
type: splitType,
linesClass: 'split-line',
wordsClass: 'split-word',
charsClass: 'split-char'
});
// Assign targets based on splitType
if (splitType.includes('chars') && splitInstance.chars && splitInstance.chars.length) {
targets = splitInstance.chars;
} else if (splitType.includes('words') && splitInstance.words && splitInstance.words.length) {
targets = splitInstance.words;
} else if (splitType.includes('lines') && splitInstance.lines && splitInstance.lines.length) {
targets = splitInstance.lines;
}
if (targets.length > 0) {
gsap.fromTo(
targets,
from,
{
...to,
duration,
ease,
stagger: delay / 1000,
scrollTrigger: {
trigger: el,
start,
once: true
},
onComplete: () => {
animationCompletedRef.current = true;
if (onLetterAnimationComplete) {
onLetterAnimationComplete();
}
}
}
);
}
el._rbsplitInstance = splitInstance;
return () => {
ScrollTrigger.getAll().forEach(st => {
if (st.trigger === el) st.kill();
});
try {
if (splitInstance && splitInstance.revert) {
splitInstance.revert();
}
} catch (_) { }
el._rbsplitInstance = undefined;
};
},
{
dependencies: [
text,
delay,
duration,
ease,
splitType,
JSON.stringify(from),
JSON.stringify(to),
threshold,
rootMargin,
fontsLoaded
],
scope: ref
}
);
const style: React.CSSProperties = {
textAlign,
display: 'inline-block',
whiteSpace: 'normal',
wordWrap: 'break-word'
};
const classes = `split-parent ${className}`;
switch (tag) {
case 'h1':
return <h1 ref={ref as any} style={style} className={classes}>{text}</h1>;
case 'h2':
return <h2 ref={ref as any} style={style} className={classes}>{text}</h2>;
case 'h3':
return <h3 ref={ref as any} style={style} className={classes}>{text}</h3>;
case 'h4':
return <h4 ref={ref as any} style={style} className={classes}>{text}</h4>;
case 'h5':
return <h5 ref={ref as any} style={style} className={classes}>{text}</h5>;
case 'h6':
return <h6 ref={ref as any} style={style} className={classes}>{text}</h6>;
case 'span':
return <span ref={ref as any} style={style} className={classes}>{text}</span>;
default:
return <p ref={ref as any} style={style} className={classes}>{text}</p>;
}
};
// Export with proper name
export const PopupText = PopupTextComponent;
// ============= STORYBOOK META =============
const meta: Meta<typeof PopupText> = {
title: 'Text/PopupText',
component: PopupText,
parameters: {
layout: 'centered',
},
argTypes: {
text: {
control: 'text',
description: 'The text content to animate'
},
delay: {
control: { type: 'number', min: 0, max: 500, step: 10 },
description: 'Delay between each character/word animation (ms)'
},
duration: {
control: { type: 'number', min: 0.1, max: 3, step: 0.1 },
description: 'Duration of each character/word animation'
},
ease: {
control: 'text',
description: 'GSAP easing function'
},
splitType: {
control: 'select',
options: ['chars', 'words', 'lines', 'words, chars'],
description: 'Type of text splitting'
},
tag: {
control: 'select',
options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span'],
description: 'HTML tag to render'
},
textAlign: {
control: 'select',
options: ['left', 'center', 'right', 'justify'],
description: 'Text alignment'
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
// ============= STORIES =============
export const Default: Story = {
args: {
text: 'Hello, MAGE UI!',
className: 'text-2xl font-semibold text-center text-white',
delay: 100,
duration: 0.6,
ease: 'power3.out',
splitType: 'chars',
from: { opacity: 0, y: 40 },
to: { opacity: 1, y: 0 },
threshold: 0.1,
rootMargin: '-100px',
textAlign: 'center',
tag: 'p',
},
};
export const WordAnimation: Story = {
args: {
text: 'The quick brown fox jumps over the lazy dog',
className: 'text-xl font-medium text-white',
splitType: 'words',
delay: 80,
duration: 0.8,
ease: 'power3.out',
tag: 'h2',
textAlign: 'left',
},
};
export const SlowReveal: Story = {
args: {
text: 'Beautiful Animations',
className: 'text-3xl font-bold text-white',
delay: 150,
duration: 1.0,
ease: 'power2.inOut',
from: { opacity: 0, y: 60, scale: 0.8 },
to: { opacity: 1, y: 0, scale: 1 },
tag: 'h1',
splitType: 'chars',
},
};
export const FastPop: Story = {
args: {
text: 'Quick Pop!',
className: 'text-4xl font-black text-white',
delay: 50,
duration: 0.4,
ease: 'back.out(1.7)',
from: { opacity: 0, scale: 0 },
to: { opacity: 1, scale: 1 },
splitType: 'chars',
tag: 'h1',
},
};