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:
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:
"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} />;