skip to main content

Imagine, code, crée, inspire.

1. Installation with the shadcn/ui CLI

Copy the command below into your terminal to install the component

pnpm dlx shadcn@latest add @envindavsorg/flip-sentences

2. Manual installation

(2.1) install the dependencies:

pnpm add motion clsx tailwind-merge

(2.2) add the cn helper:

TypeScriptlib/utils.ts
import { clsx } from "clsx";
import type { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
 
export const cn = (...inputClasses: ClassValue[]): string =>
  twMerge(clsx(inputClasses));

(2.3) create the array with the sentences you want to cycle through:

const EXAMPLE_SENTENCES = [
  "Imagine, code, crée, inspire.",
  "Chaque petit pixel compte !",
  "Chaque petit détail compte !",
  "Du concept au déploiement !",
];

(2.4) add the following code:

Reactcomponents/FlipSentences.tsx
"use client";
 
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useMemo, useState } from "react";
 
import { cn } from "@/lib/utils";
 
interface FlipSentencesProps {
  className?: string;
  sentences: string[];
  interval?: number;
  disableAnimation?: boolean;
}
 
const sentenceVariants = {
  animate: { opacity: 1, y: 0 },
  exit: { opacity: 0, y: -10 },
  initial: { opacity: 0, y: 10 },
} as const;
 
const sentenceTransition = {
  duration: 0.25,
  ease: "easeOut",
} as const;
 
export const FlipSentences = ({
  className,
  sentences,
  interval = 4000,
  disableAnimation = false,
}: FlipSentencesProps) => {
  const [currentIndex, setCurrentIndex] = useState(0);
  const sentenceCount = sentences.length;
 
  const longestSentence = useMemo(() => {
    let longest = "";
    for (const sentence of sentences) {
      if (sentence.length > longest.length) {
        longest = sentence;
      }
    }
    return longest;
  }, [sentences]);
 
  useEffect(() => {
    if (disableAnimation) {
      setCurrentIndex(0);
      return;
    }
 
    const advance = () =>
      setCurrentIndex((prev) => (prev + 1) % sentenceCount);
 
    let timer = setInterval(advance, interval);
 
    const handleVisibility = () => {
      clearInterval(timer);
      if (!document.hidden) {
        advance();
        timer = setInterval(advance, interval);
      }
    };
 
    document.addEventListener("visibilitychange", handleVisibility);
 
    return () => {
      clearInterval(timer);
      document.removeEventListener(
        "visibilitychange",
        handleVisibility
      );
    };
  }, [sentenceCount, interval, disableAnimation]);
 
  if (disableAnimation) {
    return (
      <p
        className={cn(
          "font-medium text-theme text-xs sm:text-sm",
          className
        )}
      >
        {sentences[0]}
      </p>
    );
  }
 
  return (
    <div className={cn("relative overflow-hidden", className)}>
      <AnimatePresence mode="wait">
        <motion.p
          animate="animate"
          className="font-medium text-theme text-xs sm:text-sm"
          exit="exit"
          initial="initial"
          key={currentIndex}
          transition={sentenceTransition}
          variants={sentenceVariants}
        >
          {sentences[currentIndex]}
        </motion.p>
      </AnimatePresence>
 
      <span aria-hidden="true" className="invisible block h-0">
        {longestSentence}
      </span>
    </div>
  );
};

(2.5) a little tip:

Update the import paths of your dependencies to match your project's structure so it works correctly.

3. Usage

"use client";
 
import { FlipSentences } from "@/components/flip-sentences";
 
<FlipSentences sentences={EXAMPLE_SENTENCES} />;

4. References