wui
组件

文本循环 Text Loop

在多段文本或行内内容之间自动循环切换。

基础示例

Designed to be clear.

pnpm dlx wui@latest add @wui/text-loop
components/ui/text-loop.tsx
"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 节点,但应保持相近尺寸,避免页面布局不断跳动。

组件属性

PropTypeDefaultDescription
children *ReactNode[]Content items displayed one after another.
intervalnumber2.5Seconds between item changes.
transitionTransition{ duration: 0.35, ease: "easeOut" }Motion transition for each item.
variantsVariants{ 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.
triggerbooleantrueStart or pause automatic changes.
mode"sync" | "popLayout" | "wait"popLayoutAnimatePresence sequencing mode.

事件

onIndexChange 在索引切换后返回新索引。trigger={false} 会暂停自动轮换,原生 <span> 事件照常透传。

扩展使用

可通过 variants 自定义切换方向,通过 mode 调整新旧内容的排序策略。持续循环的文字不应承载必须被读屏即时获知的重要状态。