Build a Rotating Text Component in React
Cycle through a list of words with a smooth rotation or slide animation, ideal for dynamic hero taglines in React.
"I design things." is fine. "I design websites / apps / experiences" cycling one after another is far more interesting. Rotating Text is the simplest way to fit multiple ideas into a single fixed space without a carousel.
The final result
RotatingText cycles through an array of strings at a configurable interval. Each string animates in from below while the previous one exits upward. Characters can be split and staggered individually. The component exposes an imperative API via ref so you can control it programmatically.
Setting up
This component uses Motion's AnimatePresence to handle simultaneous enter and exit animations.
import { motion, AnimatePresence } from 'motion/react';
import { forwardRef, useImperativeHandle, useCallback, useEffect, useState, useMemo } from 'react';Building the component
The component is wrapped in forwardRef to expose the imperative API. The handle gives you next, previous, jumpTo, and reset methods.
useImperativeHandle(ref, () => ({ next, previous, jumpTo, reset }), [next, previous, jumpTo, reset]);The elements array is built from the current text string. In splitBy="characters" mode, we use Intl.Segmenter to split the string correctly, which handles emoji and multi-byte characters that Array.from(text) would mangle.
const splitIntoCharacters = (text: string): string[] => {
if (typeof Intl !== 'undefined' && Intl.Segmenter) {
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
return Array.from(segmenter.segment(text), segment => segment.segment);
}
return Array.from(text);
};The stagger delay function supports several stagger origins: from the first character, from the last, from the center, or from a random position.
const getStaggerDelay = (index: number, totalChars: number): number => {
if (staggerFrom === 'first') return index * staggerDuration;
if (staggerFrom === 'last') return (totalChars - 1 - index) * staggerDuration;
if (staggerFrom === 'center') {
const center = Math.floor(totalChars / 2);
return Math.abs(center - index) * staggerDuration;
}
if (staggerFrom === 'random') {
const randomIndex = Math.floor(Math.random() * totalChars);
return Math.abs(randomIndex - index) * staggerDuration;
}
return Math.abs((staggerFrom as number) - index) * staggerDuration;
};AnimatePresence with mode="wait" ensures the exit animation completes before the new string starts entering.
<AnimatePresence mode={animatePresenceMode} initial={animatePresenceInitial}>
<motion.span key={currentTextIndex} layout aria-hidden='true'>
{elements.map((wordObj, wordIndex, array) => {
const previousCharsCount = array
.slice(0, wordIndex)
.reduce((sum, word) => sum + word.characters.length, 0);
return (
<span key={wordIndex} className='inline-flex'>
{wordObj.characters.map((char, charIndex) => (
<motion.span
key={charIndex}
initial={initial}
animate={animate}
exit={exit}
transition={{
...transition,
delay: getStaggerDelay(previousCharsCount + charIndex, totalChars),
}}
className='inline-block'
>
{char}
</motion.span>
))}
</span>
);
})}
</motion.span>
</AnimatePresence>How to use it
<RotatingText
texts={['designers', 'developers', 'founders']}
rotationInterval={2000}
staggerDuration={0.025}
staggerFrom="first"
initial={{ y: '100%', opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: '-120%', opacity: 0 }}
/>Access the imperative API by passing a ref and calling ref.current.next() from a button or any other trigger.
Key takeaways
- Using
Intl.Segmenterfor character splitting is the correct approach in 2025. It handles emoji, combining characters, and other multi-codepoint graphemes that simple string iteration would split incorrectly. - Keeping a screen-reader-only
<span>with the current text alongside the animated visual output ensures the rotating text is accessible to assistive technology. - The
animatePresenceMode="wait"option is what prevents the enter and exit animations from overlapping, which would look like two texts fighting for the same space.