Docs
Rotating Text
Rotating Text
Rotation of texts while scaling at the same time.
Scroll to see the animation
Hello, World!
This is a sample text.
Install the following dependencies:
pnpm add gsap @gsap/react
Make a file for cn and mergerRef functions and match the import afterwards
import clsx, { ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes))Make a file and copy paste this code in a file with name rotating-text.tsx
"use client"
import { RefObject, useEffect, useRef, useState } from "react"
import gsap from "gsap"
import ScrollTrigger from "gsap/ScrollTrigger"
import { useGSAP } from "@gsap/react"
import { cn } from "@/lib/utils"
gsap.registerPlugin(ScrollTrigger)
interface RotatingTextProps {
text: { data: string; className?: string }[]
scrollerRef?: RefObject<HTMLElement>
start?: string | number | ScrollTrigger.StartEndFunc
end?: string | number | ScrollTrigger.StartEndFunc
scrub?: number | boolean
markers?: boolean | ScrollTrigger.MarkersVars
className?: string
}
const RotatingText = ({
text,
scrollerRef,
start = "top top",
end = "+=300",
scrub = 1,
markers = false,
className,
}: RotatingTextProps) => {
const mainRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLSpanElement[]>([])
const instanceIdRef = useRef<string>(
`rotating-text-${Math.random().toString(36).substring(2, 11)}`
)
const [forceUpdate, setForceUpdate] = useState(false)
useEffect(() => {
if (scrollerRef?.current) {
setForceUpdate(!forceUpdate)
}
}, [scrollerRef?.current])
useGSAP(() => {
if (!mainRef.current && !textRef.current) return
// Kill only this component's ScrollTrigger instance if it exists
const existingTrigger = ScrollTrigger.getById(instanceIdRef.current)
if (existingTrigger) {
existingTrigger.kill()
}
const tl = gsap.timeline({
scrollTrigger: {
trigger: mainRef.current,
start,
end,
scrub,
pin: true,
scroller: scrollerRef?.current ?? window,
markers,
id: instanceIdRef.current,
},
})
tl.set(textRef.current, {
rotationY: (index) =>
index % 2 === 0
? gsap.utils.random(150, 180, 1)
: gsap.utils.random(-180, -150, 1),
scale: 0,
transformOrigin: (index) => (index % 2 === 0 ? "bottom" : "top"),
})
tl.to(textRef.current, {
scale: 1,
rotationY: 0,
ease: "none",
stagger: {
amount: 0.5,
from: "random",
},
})
return () => {
// Cleanup ScrollTrigger instances when component unmounts
const triggerToKill = ScrollTrigger.getById(instanceIdRef.current)
if (triggerToKill) {
triggerToKill.kill()
}
}
}, [text, start, end, scrub, markers, forceUpdate])
const charOffsets = text.reduce<number[]>((acc, item, i) => {
const prev = acc[i - 1] ?? 0
const newCount = item.data.length + prev
return [...acc, newCount]
}, [])
return (
<div
ref={mainRef}
className={cn(
" h-screen text-9xl",
className,
" flex justify-center items-center "
)}
style={{ perspective: "800px" }}
>
<div>
{text.map((t, rowIndex) => (
<div key={rowIndex} className={cn(t.className, "text-center")}>
{t.data.split("").map((char, charIndex) => {
const globalIndex =
charIndex + (rowIndex > 0 ? charOffsets[rowIndex - 1] : 0)
return (
<span
key={charIndex}
style={{
display: "inline-block",
transformStyle: "preserve-3d",
}}
ref={(el) => {
if (el) textRef.current[globalIndex] = el
}}
>
{char === " " ? "\u00A0" : char}
</span>
)
})}
</div>
))}
</div>
</div>
)
}
export RotatingTextProps
| Name | Type | Description |
|---|---|---|
text* | { data: string, className?: string }[] | Array of text objects with data and optional class names for each text block. |
className | string | Additional class names for styling the main container. |
scrollerRef | RefObject<HTMLElement> | Scroll container reference for ScrollTrigger. |
start | string | number | ScrollTrigger.StartEndFunc | Start point for the ScrollTrigger animation. |
end | string | number | ScrollTrigger.StartEndFunc | End point for the ScrollTrigger animation. |
scrub | number | boolean | Controls the smoothness of the animation linked to scroll position. |
markers | boolean | ScrollTrigger.MarkersVars | Toggle or configure markers to visualize ScrollTrigger start and end points. |
Credit: This component is inspired by Saisei.