Word Cycle
The word changes out of a blur, and the sentence never jumps in width.
Preview
"use client";
import { useDemo } from "../demo-context";
import { WordCycle } from "./WordCycle";
export default function Demo() {
const { play, auto } = useDemo();
return (
<p style={{ margin: 0, fontFamily: "var(--fluid-font)", fontSize: "clamp(26px, 9cqi, 44px)", lineHeight: 1.05, fontWeight: 600, letterSpacing: "-0.035em", color: "var(--fluid-text)", textAlign: "center" }}>
Made for <br />
<span style={{ color: "var(--fluid-accent)" }}>
<WordCycle words={["shops.", "clinics.", "schools.", "you."]} every={1.3} play={play && auto} />
</span>
</p>
);
}Installation
npx @usefluid/cli add word-cyclepnpm dlx @usefluid/cli add word-cycleyarn dlx @usefluid/cli add word-cyclebunx @usefluid/cli add word-cycleInstall the engine.
Terminal npm i @usefluid/motionCopy these files into your project.
components/fluid/word-cycle/WordCycle.tsx "use client"; import { useReducedMotion } from "@usefluid/motion/react"; import { useEffect, useState } from "react"; import styles from "./WordCycle.module.css"; type Props = { words: string[]; every?: number; loop?: boolean; play?: boolean; }; export function WordCycle({ words, every = 1.6, loop = true, play = true }: Props) { const reduced = useReducedMotion(); const [index, setIndex] = useState(0); const [leaving, setLeaving] = useState<number | null>(null); const last = words.length - 1; const finished = !loop && index >= last; useEffect(() => { if (!play || reduced || finished || words.length < 2) return; const timer = setInterval(() => { setIndex((i) => { if (!loop && i >= last) return i; setLeaving(i); return i + 1 <= last ? i + 1 : 0; }); }, every * 1000); return () => clearInterval(timer); }, [play, reduced, finished, every, loop, last, words.length]); const current = reduced ? last : index; const out = reduced ? null : leaving; return ( <span className={styles.root}> <span className={styles.srOnly}>{words[last]}</span> <span className={styles.stack} aria-hidden> {words.map((word) => ( <span key={word} className={styles.sizer}>{word}</span> ))} <span key={`in-${current}`} className={out === null ? styles.still : styles.in} onAnimationEnd={() => setLeaving(null)} > {words[current]} </span> {out !== null && <span key={`out-${out}`} className={styles.out}>{words[out]}</span>} </span> </span> ); }components/fluid/word-cycle/WordCycle.module.css .root { display: inline-block; } .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } .stack { display: inline-grid; } .stack > * { grid-area: 1 / 1; white-space: nowrap; } .sizer { visibility: hidden; } .still { display: block; } .in { display: block; animation: in 340ms cubic-bezier(0.22, 1, 0.36, 1) both; } .out { display: block; pointer-events: none; animation: out 200ms cubic-bezier(0.22, 1, 0.36, 1) both; } @keyframes in { from { opacity: 0; filter: blur(7px); transform: translateY(0.3em); } to { opacity: 1; filter: blur(0); transform: none; } } @keyframes out { from { opacity: 1; filter: blur(0); transform: none; } to { opacity: 0; filter: blur(7px); transform: translateY(-0.3em); } }Import the theme once, then point its variables at your own tokens.
layout.tsx import "@usefluid/motion/theme.css";
Usage
import { WordCycle } from "./WordCycle";
<h1>Made for <WordCycle words={["shops.", "clinics.", "you."]} loop={false} /></h1>API reference
| Prop | Type | Default | Description |
|---|---|---|---|
words | string[] | — | Short words. The box takes the width of the longest. |
every | number | 1.6 | Seconds per word. |
loop | boolean | true | False stops on the last word. |
play | boolean | true |
Keyboard and screen readers
Screen readers and reduced motion get the last word only.
Notes
Words do not wrap, so keep them to one or two words. In the product, prefer loop={false}.
Every colour comes from the --fluid-* variables, and the piece is yours to change.
How it is made
Every word sits stacked in the same grid cell, invisible, so the sentence keeps the width of the longest one. The new word comes in from a blur and the old one leaves upwards. Screen readers hear a single word.
For your agent
For your agent
Add Word Cycle to my project with the Fluid library. Use WordCycle in the main headline, with words that describe my audience. Read my project first. Use my logo, my colours and my font through the --fluid-* variables. Keep 44px touch targets, visible focus and reduced motion. If I have no SVG logo, ask before making one up.