Animated Text
zyeon/animated-textFreeComponent
Character-level text animation — typewriter, scramble and flowing gradient variants in one component.
See it running
Open the demo on zyeon
ui.zyeon.ai/r/animated-textInstall it
npx shadcn@latest add https://ui.zyeon.ai/r/animated-text.jsonSource
1"use client"23import * as React from "react"4import { cn } from "@/lib/utils"56const KEYFRAMES = `@keyframes at-gradient{to{background-position:200% center}}`78const SCRAMBLE_CHARS = "!<>-_\\/[]{}=+*^?#"910// 渲染必须是纯函数(react-hooks/purity):用 (step, i) 的确定性哈希代替 Math.random,11// step 每 tick 变化,乱序观感不变12function scrambleChar(step: number, i: number) {13 return SCRAMBLE_CHARS[(Math.imul(step * 31 + i * 17 + 7, 2654435761) >>> 0) % SCRAMBLE_CHARS.length]14}1516export type AnimatedTextVariant = "typewriter" | "scramble" | "gradient"1718export interface AnimatedTextProps extends React.HTMLAttributes<HTMLSpanElement> {19 text: string20 variant?: AnimatedTextVariant21 /** ms per character step (typewriter / scramble) */22 speed?: number23 /** ms before the animation starts */24 delay?: number25}2627function subscribeReducedMotion(callback: () => void) {28 const mq = window.matchMedia("(prefers-reduced-motion: reduce)")29 mq.addEventListener("change", callback)30 return () => mq.removeEventListener("change", callback)31}3233function useReducedMotion() {34 return React.useSyncExternalStore(35 subscribeReducedMotion,36 () => window.matchMedia("(prefers-reduced-motion: reduce)").matches,37 () => false,38 )39}4041export const AnimatedText = React.forwardRef<HTMLSpanElement, AnimatedTextProps>(42 ({ text, variant = "typewriter", speed = 45, delay = 0, className, ...props }, ref) => {43 const reduced = useReducedMotion()44 // 已定字符数;显示文本在渲染期派生,effect 只管计时器45 const [progress, setProgress] = React.useState(0)4647 // props 变化时在渲染期重置进度(React 官方 adjust-state-during-render 模式)48 const resetKey = `${variant}|${speed}|${delay}|${text}`49 const [prevKey, setPrevKey] = React.useState(resetKey)50 if (prevKey !== resetKey) {51 setPrevKey(resetKey)52 setProgress(0)53 }5455 React.useEffect(() => {56 if (variant === "gradient" || reduced) return57 let interval: ReturnType<typeof setInterval> | undefined58 const timeout = setTimeout(() => {59 interval = setInterval(() => {60 setProgress(p => {61 if (p + 1 >= text.length) clearInterval(interval)62 return Math.min(p + 1, text.length)63 })64 }, speed)65 }, delay)66 return () => {67 clearTimeout(timeout)68 if (interval) clearInterval(interval)69 }70 }, [text, variant, speed, delay, reduced])7172 if (variant === "gradient") {73// … truncated
What it pulls in
Other registry items
Files it writes
More from zyeon
All 517 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Accordionaccordion | zyeon | Components | Free | 1 dep | |
| Activity Heatmapactivity-heatmap | zyeon | Components | Paid | 1 dep · 2 files | |
| Address Formaddress-form | zyeon | Components | Paid | 1 dep | |
| Agent Graphagent-graph | zyeon | Components | Free | 2 deps · 2 files | |
| Agent Handoffagent-handoff | zyeon | Components | Free | 1 dep | |
| Agent Inboxagent-inbox | zyeon | Components | Free | 2 deps · 2 files | |
| Agent Permissionsagent-permissions | zyeon | Components | Free | 2 deps · 2 files | |
| Agent Planagent-plan | zyeon | Components | Free | 2 deps · 2 files |
