wui
组件

文本翻滚 Text Roll

悬停或挂载时逐字符翻滚的文字效果。

基础示例

Loading…
pnpm dlx wui@latest add @wui/text-roll
components/ui/text-roll.tsx
"use client"

import * as React from "react"
import {
  motion,
  useReducedMotion,
  type Transition,
  type Variants,
} from "motion/react"

import { cn } from "@/lib/utils"

const defaultVariants: Variants = {
  rest: { y: "0%" },
  hover: { y: "-50%" },
}

export interface TextRollProps extends React.ComponentProps<"span"> {
  /** Text rolled character by character. */
  children: string
  /** Duration of each character roll in seconds. @default 0.45 */
  duration?: number
  /** Delay for each character entering the roll. */
  getEnterDelay?: (index: number) => number
  /** Delay for each character returning to rest. */
  getExitDelay?: (index: number) => number
  /** Motion transition merged into every character. */
  transition?: Transition
  /** Rest and hover states for each character track. */
  variants?: Variants
  /** Play on hover or immediately on mount. @default "hover" */
  trigger?: "hover" | "mount"
}

/** Rolls a second copy of each character into view. */
function TextRoll({
  children,
  className,
  duration = 0.45,
  getEnterDelay = (index) => index * 0.025,
  getExitDelay = (index) => index * 0.02,
  transition,
  variants = defaultVariants,
  trigger = "hover",
  onMouseEnter,
  onMouseLeave,
  ...props
}: TextRollProps) {
  const reduceMotion = useReducedMotion()
  const [hovered, setHovered] = React.useState(false)
  const active = trigger === "mount" || hovered

  return (
    <span
      aria-label={children}
      data-slot="text-roll"
      className={cn("inline-flex", className)}
      onMouseEnter={(event) => {
        setHovered(true)
        onMouseEnter?.(event)
      }}
      onMouseLeave={(event) => {
        setHovered(false)
        onMouseLeave?.(event)
      }}
      {...props}
    >
      {Array.from(children).map((character, index) => (
        <span
          aria-hidden="true"
          data-slot="text-roll-character"
          className="inline-block h-[1em] overflow-hidden leading-none"
          key={`${character}-${index}`}
        >
          <motion.span
            className="flex flex-col"
            variants={variants}
            initial="rest"
            animate={reduceMotion ? "rest" : active ? "hover" : "rest"}
            transition={{
              duration,
              ease: [0.22, 1, 0.36, 1],
              delay: active ? getEnterDelay(index) : getExitDelay(index),
              ...transition,
            }}
          >
            <span className="block h-[1em] whitespace-pre">
              {character === " " ? "\u00a0" : character}
            </span>
            <span className="block h-[1em] whitespace-pre">
              {character === " " ? "\u00a0" : character}
            </span>
          </motion.span>
        </span>
      ))}
    </span>
  )
}

export { TextRoll }

组件作用

TextRoll 复制每个字符并纵向滚动第二份内容,适合导航链接、简短按钮和展示标题。默认悬停触发,不会持续争夺注意力。

组件属性

PropTypeDefaultDescription
children *stringText rolled character by character.
durationnumber0.45Duration of each character roll in seconds.
getEnterDelay((index: number) => number)(index) => index * 0.025Delay for each character entering the roll.
getExitDelay((index: number) => number)(index) => index * 0.02Delay for each character returning to rest.
transitionTransitionMotion transition merged into every character.
variantsVariants{ rest: { y: "0%" }, hover: { y: "-50%" }, }Rest and hover states for each character track.
trigger"hover" | "mount"hoverPlay on hover or immediately on mount.

事件

组件使用鼠标进入和离开控制默认效果,其余 <span> 事件继续透传。减少动态效果时保留静态文字。

扩展使用

使用 trigger="mount" 可在挂载时播放,getEnterDelaygetExitDelay 用来改变字符波次。自定义 variants 时保留 resthover 两个状态名。