Special Text
spell-ui/special-textFreeComponent
Animated text with scramble effect.
Live preview
live · rendered by the registry
Rendered by spell-ui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://spell.sh/r/special-text.jsonSource
1"use client";23import { useEffect, useRef, useState } from "react";4import { useInView } from "motion/react";56interface SpecialTextProps {7 children: string;8 speed?: number;9 delay?: number;10 className?: string;11 inView?: boolean;12 once?: boolean;13}1415const RANDOM_CHARS = "_!X$0-+*#";1617function getRandomChar(prevChar?: string): string {18 let char: string;19 do {20 char = RANDOM_CHARS[Math.floor(Math.random() * RANDOM_CHARS.length)];21 } while (char === prevChar);22 return char;23}2425export function SpecialText({26 children,27 speed = 20,28 delay = 0,29 className = "",30 inView = false,31 once = true,32}: SpecialTextProps) {33 const containerRef = useRef<HTMLSpanElement>(null);34 const isInView = useInView(containerRef, { once, margin: "-100px" });35 const shouldAnimate = inView ? isInView : true;36 const [hasStarted, setHasStarted] = useState(() => !inView && delay <= 0);37 const text = children;38 const [displayText, setDisplayText] = useState<string>(39 " ".repeat(text.length),40 );41 const [currentPhase, setCurrentPhase] = useState<"phase1" | "phase2">(42 "phase1",43 );44 const [animationStep, setAnimationStep] = useState<number>(0);45 const intervalRef = useRef<NodeJS.Timeout | null>(null);46 const startTimeoutRef = useRef<number | null>(null);4748 function clearStartTimeout() {49 if (startTimeoutRef.current === null) return;50 window.clearTimeout(startTimeoutRef.current);51 startTimeoutRef.current = null;52 }5354 function startAnimation() {55 setHasStarted(true);56 setDisplayText(" ".repeat(text.length));57 setCurrentPhase("phase1");58 setAnimationStep(0);59 }6061 const runPhase1 = () => {62 const maxSteps = text.length * 2;63 const currentLength = Math.min(animationStep + 1, text.length);6465 const chars: string[] = [];66 for (let i = 0; i < currentLength; i++) {67 const prevChar = i > 0 ? chars[i - 1] : undefined;68 chars.push(getRandomChar(prevChar));69 }7071 for (let i = currentLength; i < text.length; i++) {72 chars.push("\u00A0");73 }7475 setDisplayText(chars.join(""));7677 if (animationStep < maxSteps - 1) {78 setAnimationStep((prev) => prev + 1);79 } else {80 setCurrentPhase("phase2");81 setAnimationStep(0);82 }83 };8485 const runPhase2 = () => {86 const revealedCount = Math.floor(animationStep / 2);87 const chars: string[] = [];8889 for (let i = 0; i < revealedCount && i < text.length; i++) {90 chars.push(text[i]);91 }9293 if (revealedCount < text.length) {94// … truncated
Files it writes
More from spell-ui
All 33 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Animated Checkboxanimated-checkbox | spell-ui | Components | Free | 1 dep | |
| Animated Gradientanimated-gradient | spell-ui | Components | Free | no deps | |
| Badgebadge | spell-ui | Components | Free | 2 deps | |
| Bars Spinnerbars-spinner | spell-ui | Components | Free | no deps | |
| Blur Revealblur-reveal | spell-ui | Components | Free | 1 dep | |
| Chartchart | spell-ui | Components | Free | no deps | |
| Color Selectorcolor-selector | spell-ui | Components | Free | no deps | |
| Copy Buttoncopy-button | spell-ui | Components | Free | 1 dep |
