Skip to content
jjswnth/ui

Text

Flip Words

A word carousel that rolls through a list without shifting the line around it.

Build interfaces that feel effortless

Installation

  1. 1. Install the dependencies.

    terminal
    npm install motion clsx tailwind-merge
  2. 2. Add the cn helper, if you don't already have it.

    lib/utils.ts
    import { clsx, type ClassValue } from "clsx";
    import { twMerge } from "tailwind-merge";
    
    export function cn(...inputs: ClassValue[]) {
      return twMerge(clsx(inputs));
    }
  3. 3. Copy the component into your project.

    components/ui/flip-words.tsx
    "use client";
    
    import { AnimatePresence, motion } from "motion/react";
    import { useEffect, useState } from "react";
    import { cn } from "@/lib/utils";
    
    type FlipWordsProps = {
      words: string[];
      className?: string;
      /** Milliseconds each word stays on screen. */
      interval?: number;
    };
    
    export function FlipWords({ words, className, interval = 2200 }: FlipWordsProps) {
      const [index, setIndex] = useState(0);
    
      useEffect(() => {
        const id = window.setInterval(
          () => setIndex((current) => (current + 1) % words.length),
          interval,
        );
        return () => window.clearInterval(id);
      }, [interval, words.length]);
    
      const word = words[index] ?? "";
    
      return (
        <span className="relative inline-flex overflow-hidden align-bottom">
          {/* Reserves the width of the longest word so surrounding text never jumps. */}
          <span aria-hidden className={cn("invisible whitespace-pre", className)}>
            {words.reduce((a, b) => (b.length > a.length ? b : a), "")}
          </span>
          <AnimatePresence mode="wait">
            <motion.span
              key={word}
              initial={{ y: "100%", opacity: 0 }}
              animate={{ y: 0, opacity: 1 }}
              exit={{ y: "-100%", opacity: 0 }}
              transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
              className={cn("absolute inset-0 whitespace-pre", className)}
            >
              {word}
            </motion.span>
          </AnimatePresence>
        </span>
      );
    }

Props

PropTypeDefaultDescription
wordsstring[]Words to cycle through, in order.
intervalnumber2200Milliseconds each word stays on screen.
classNamestringApplied to the visible word and its width placeholder.