Studio KitAdvanced Text Animationsmotion/react

RotatingWords

Static prefix followed by a slot that springs through a list of accent-colored words on an interval, reserving the widest word's width so nothing around it shifts.

Preview

Ship faster, smoother, bolder, today

Customize

4 props
2000

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add rotating-words

Writes components/ui/rotating-words.tsx and components/styles/rotating-words.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { RotatingWords } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <RotatingWords />6  );7}

Props

PropTypeDefault
prefix

Static text rendered before the rotating word.

string'Ship'
words

Words cycled in place.

string[]['faster', 'smoother', 'bolder', 'today']
interval

Milliseconds each word stays visible.

number2000
accentColor

Color of the rotating word.

string'#38bdf8'
className

Additional class merged onto the root element.

string''

Accessibility

A visually hidden span reads the prefix and full word list once; the animated slot is aria-hidden. The slot width is reserved by invisible sizer copies, preventing layout shift. Under prefers-reduced-motion the interval is not started and the first word is shown statically.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/rotating-words.tsx
1"use client";2 3import * as React from "react";4import { AnimatePresence, motion, useReducedMotion } from "motion/react";5import styles from "../styles/rotating-words.module.css";6 7export interface RotatingWordsProps {8  /** Static text rendered before the rotating word */9  prefix?: string;10  /** Words cycled in place */11  words?: string[];12  /** Milliseconds each word stays visible */13  interval?: number;14  /** Color of the rotating word */15  accentColor?: string;16  /** Optional className */17  className?: string;18}19 20const DEFAULT_WORDS = ["faster", "smoother", "bolder", "today"];21 22export function RotatingWords({23  prefix = "Ship",24  words = DEFAULT_WORDS,25  interval = 2000,26  accentColor = "#38bdf8",27  className = "",28}: RotatingWordsProps) {29  const prefersReducedMotion = useReducedMotion();30  const list = words.length > 0 ? words : DEFAULT_WORDS;31  const [index, setIndex] = React.useState(0);32 33  React.useEffect(() => {34    if (prefersReducedMotion || list.length < 2) return;35    const id = setInterval(() => setIndex((n) => (n + 1) % list.length), interval);36    return () => clearInterval(id);37  }, [list.length, interval, prefersReducedMotion]);38 39  const current = list[index % list.length];40  const label = `${prefix ? `${prefix} ` : ""}${list.join(", ")}`;41 42  return (43    <span44      style={{ "--mf-rw-accent": accentColor } as React.CSSProperties}45      className={`${styles.root} ${className}`.trim()}46    >47      <span className={styles.srOnly}>{label}</span>48      <span aria-hidden="true" className={styles.visual}>49        {prefix ? <span className={styles.prefix}>{prefix}</span> : null}50        <span className={styles.slot}>51          {list.map((word, i) => (52            <span key={`sizer-${i}`} className={styles.sizer}>53              {word}54            </span>55          ))}56          <AnimatePresence mode="wait" initial={false}>57            <motion.span58              key={`${index}-${current}`}59              className={styles.word}60              initial={prefersReducedMotion ? false : { y: "100%", opacity: 0 }}61              animate={{ y: 0, opacity: 1 }}62              exit={prefersReducedMotion ? undefined : { y: "-100%", opacity: 0 }}63              transition={{ type: "spring", stiffness: 300, damping: 25 }}64            >65              {current}66            </motion.span>67          </AnimatePresence>68        </span>69      </span>70    </span>71  );72}