Rotating Words

One word in a headline slides up and out while the next slides in from below, and the slot resizes to fit it.

  • CSS
  • Text
  • Loop
  • Added

Design that feels calm

AI prompt

Build a React headline where the last word rotates: "Design that feels" followed by calm, precise, alive and yours. Every 2.2s advance an index. Render the words stacked in an inline-grid cell with overflow hidden; the active word sits at translateY(0) and opacity 1, the previous word moves to translateY(-100%) and opacity 0, and the rest wait at translateY(100%). Transition transform and opacity over 500ms with cubic-bezier(0.16, 1, 0.3, 1). Animate the slot width to the active word: measure it with a hidden copy and set width with a 300ms transition. Colour the rotating word with the accent. Keep the full sentence with the current word in an aria-live="polite" visually hidden element. Under prefers-reduced-motion, swap words with a 150ms fade.

Source

Demo.tsx
'use client'

import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import s from './demo.module.css'

export default function RotatingWords({ labels }: { labels: Record<string, string> }) {
  const words = labels.words.split('|')
  const [index, setIndex] = useState(0)
  const [width, setWidth] = useState<number>()
  const measure = useRef<HTMLSpanElement>(null)

  useEffect(() => {
    const id = setInterval(() => setIndex((i) => (i + 1) % words.length), 2200)
    return () => clearInterval(id)
  }, [words.length])

  useLayoutEffect(() => {
    if (measure.current) setWidth(measure.current.offsetWidth)
  }, [index, labels.words])

  const prev = (index - 1 + words.length) % words.length

  return (
    <p className={s.line}>
      <span aria-hidden="true">{labels.lead} </span>
      <span className={s.slot} style={{ width }} aria-hidden="true">
        {words.map((w, i) => (
          <span key={w} className={s.word} data-state={i === index ? 'in' : i === prev ? 'out' : 'wait'}>
            {w}
          </span>
        ))}
      </span>
      <span ref={measure} className={s.measure} aria-hidden="true">
        {words[index]}
      </span>
      <span className={s.sr} aria-live="polite">
        {labels.lead} {words[index]}
      </span>
    </p>
  )
}
demo.module.css
.line {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: 0.25em;
  margin: 0;
  font: 600 var(--text-2xl) / 1.2 var(--font-display);
  letter-spacing: -0.02em;
  color: var(--stage-ink);
  white-space: nowrap;
}

.slot {
  display: inline-grid;
  overflow: hidden;
  transition: width 300ms var(--ease-out);
}

.word {
  grid-area: 1 / 1;
  color: var(--stage-accent);
  transition: transform 500ms var(--ease-out), opacity 500ms var(--ease-out);
}

.word[data-state='in'] { transform: translateY(0); opacity: 1; }
.word[data-state='out'] { transform: translateY(-100%); opacity: 0; }
.word[data-state='wait'] { transform: translateY(100%); opacity: 0; transition: none; }

.measure { position: absolute; visibility: hidden; white-space: nowrap; }

.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }

@media (prefers-reduced-motion: reduce) {
  .word, .word[data-state] { transform: none; transition: opacity 150ms linear; }
  .slot { transition: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.