组件
文本循环 Text Loop
在多段文本或行内内容之间自动循环切换。
基础示例
Designed to be clear.
pnpm dlx wui@latest add @wui/text-loop"use client"
import * as React from "react"
import {
AnimatePresence,
motion,
useReducedMotion,
type AnimatePresenceProps,
type Transition,
type Variants,
} from "motion/react"
import { cn } from "@/lib/utils"
const defaultVariants: Variants = {
initial: { y: 12, opacity: 0, filter: "blur(4px)" },
animate: { y: 0, opacity: 1, filter: "blur(0px)" },
exit: { y: -12, opacity: 0, filter: "blur(4px)" },
}
export interface TextLoopProps extends React.ComponentProps<"span"> {
/** Content items displayed one after another. */
children: React.ReactNode[]
/** Seconds between item changes. @default 2.5 */
interval?: number
/** Motion transition for each item. */
transition?: Transition
/** Initial, animate and exit states. */
variants?: Variants
/** Called after the active index changes. */
onIndexChange?: (index: number) => void
/** Start or pause automatic changes. @default true */
trigger?: boolean
/** AnimatePresence sequencing mode. @default "popLayout" */
mode?: AnimatePresenceProps["mode"]
}
/** Cycles through an array of text or inline content. */
function TextLoop({
children,
className,
interval = 2.5,
transition = { duration: 0.35, ease: "easeOut" },
variants = defaultVariants,
onIndexChange,
trigger = true,
mode = "popLayout",
...props
}: TextLoopProps) {
const [index, setIndex] = React.useState(0)
const reduceMotion = useReducedMotion()
React.useEffect(() => {
if (!trigger || children.length < 2) return
const timer = window.setInterval(() => {
setIndex((current) => {
const next = (current + 1) % children.length
onIndexChange?.(next)
return next
})
}, interval * 1000)
return () => window.clearInterval(timer)
}, [children.length, interval, onIndexChange, trigger])
React.useEffect(() => {
if (index >= children.length) setIndex(0)
}, [children.length, index])
return (
<span
data-slot="text-loop"
className={cn(
"relative inline-grid overflow-hidden align-bottom",
className
)}
{...props}
>
<AnimatePresence initial={false} mode={mode}>
<motion.span
data-slot="text-loop-item"
className="col-start-1 row-start-1 inline-block"
key={index}
variants={variants}
initial={reduceMotion ? false : "initial"}
animate="animate"
exit="exit"
transition={reduceMotion ? { duration: 0 } : transition}
>
{children[index]}
</motion.span>
</AnimatePresence>
</span>
)
}
export { TextLoop }
组件作用
TextLoop 用于一句话中轮换关键词、功能标签或短状态。子项可以是任意 React 节点,但应保持相近尺寸,避免页面布局不断跳动。
组件属性
| Prop | Type | Default | Description |
|---|---|---|---|
| children * | ReactNode[] | — | Content items displayed one after another. |
| interval | number | 2.5 | Seconds between item changes. |
| transition | Transition | { duration: 0.35, ease: "easeOut" } | Motion transition for each item. |
| variants | Variants | { initial: { y: 12, opacity: 0, filter: "blur(4px)" }, animate: { y: 0, opacity: 1, filter: "blur(0px)" }, exit: { y: -12, opacity: 0, filter: "blur(4px)" }, } | Initial, animate and exit states. |
| onIndexChange | ((index: number) => void) | — | Called after the active index changes. |
| trigger | boolean | true | Start or pause automatic changes. |
| mode | "sync" | "popLayout" | "wait" | popLayout | AnimatePresence sequencing mode. |
事件
onIndexChange 在索引切换后返回新索引。trigger={false} 会暂停自动轮换,原生 <span> 事件照常透传。
扩展使用
可通过 variants 自定义切换方向,通过 mode 调整新旧内容的排序策略。持续循环的文字不应承载必须被读屏即时获知的重要状态。