Text switcher
100xui/text-switcherFreeBlock
A reusable component that creates an engaging animation effect by smoothly transitioning between a list of phrases. The animation, driven by a moving dot, makes it ideal for dynamically completing a sentence or tagline.
Live preview
live · rendered by the registry
Rendered by 100xui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://100xui.com/r/text-switcher.jsonSource
1"use client";23import React from "react";4import {5 AnimatePresence,6 motion,7 useMotionValue,8 useTransform,9 useVelocity,10} from "motion/react";1112import { cn } from "@/lib/utils";1314export interface TextSwitcherProps {15 phrases: string[];16 readTimeInSec?: number;17 animationDurationInSec?: number;18 className?: string;19 style?: React.CSSProperties;20}2122const letterVariants = {23 initial: { scaleX: 0, opacity: 0 },24 animate: { scaleX: 1, opacity: 1 },25 exit: { scaleX: 0, opacity: 0 },26};2728export function TextSwitcher({29 phrases,30 animationDurationInSec = 0.4,31 readTimeInSec = 2,32 className,33 style,34}: TextSwitcherProps) {35 const [currentPhrase, setCurrentPhrase] = React.useState(0);3637 const dotLeft = useMotionValue("100%");38 const dotLeftAsFloat = useTransform(dotLeft, (latest) => parseFloat(latest));39 const dotVelocity = useVelocity(dotLeftAsFloat);4041 const dotScaleX = useTransform(dotVelocity, [-120, 0, 120], [3, 1, 3]);42 const dotColor = useTransform(dotVelocity, (latest) => {43 return Math.round(latest) !== 0 ? "var(--destructive)" : "";44 });4546 React.useEffect(() => {47 const totalCycleTimeInMs =48 (readTimeInSec + 2 * animationDurationInSec) * 1000;49 const controlInterval = setInterval(() => {50 setCurrentPhrase((prevIndex) => (prevIndex + 1) % phrases.length);51 }, totalCycleTimeInMs);5253 return () => clearInterval(controlInterval);54 }, [phrases.length, readTimeInSec, animationDurationInSec]);5556 const containerVariants = {57 animate: {58 transition: {59 staggerChildren: animationDurationInSec / phrases[currentPhrase].length,60 },61 },62 exit: {63 transition: {64 staggerChildren: animationDurationInSec / phrases[currentPhrase].length,65 staggerDirection: -1,66 },67 },68 };6970 return (71 <div72 className={cn("inline-block", className, "!relative")}73 style={{ ...style }}74 >75 <AnimatePresence initial={false} mode="wait">76 <motion.div77 key={currentPhrase}78 className="flex flex-nowrap whitespace-pre"79 variants={containerVariants}80 initial="initial"81 animate="animate"82 exit="exit"83 >84 {phrases[currentPhrase].split("").map((letter, i) => (85 <motion.div86 key={`phrases[${currentPhrase}][${i}]`}87 className="origin-left"88 variants={letterVariants}89 transition={{90 duration:91// … truncated
What it pulls in
npm packages
Files it writes
More from 100xui
All 14 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Apple galleryapple-gallery | 100xui | Blocks | Free | 1 dep | |
| Chat bento cardchat-bento-card | 100xui | Blocks | Free | 1 dep | |
| demosdemos | 100xui | Blocks | Free | no deps · 12 files | |
| In-page navbarin-page-navbar | 100xui | Blocks | Free | 1 dep · 2 files | |
| internalsinternals | 100xui | Blocks | Free | no deps · 4 files | |
| iPhone 3D mockupiphone-3d-mockup | 100xui | Blocks | Free | no deps | |
| Menu wheelmenu-wheel | 100xui | Blocks | Free | 1 dep | |
| Morph modalmorph-modal | 100xui | Blocks | Free | 1 dep |
