wui
组件

悬停跑马灯 Hover Marquee

当列表行被悬停或聚焦时,原地替换为水平无限循环滚动的跑马灯轨道预览。

第三方依赖 · motion

基础用法

鼠标悬停在列表项上,静态标题将上移淡出,并原地切入水平无缝循环滚动的跑马灯轨道:

Loading…

安装与引入

通过 CLI 自动添加组件,或手动复制源码至项目中:

pnpm dlx @wui-design/cli@latest add @wui/hover-marquee
安装基础依赖与动效库
pnpm add motion lucide-react class-variance-authority clsx tailwind-merge
复制组件源码到 components/ui/hover-marquee.tsx
components/ui/hover-marquee.tsx
"use client"

import * as React from "react"
import {
  AnimatePresence,
  motion,
  useReducedMotion,
  type HTMLMotionProps,
} from "motion/react"

import { InfiniteSlider } from "@/components/ui/infinite-slider"
import { cn } from "@/lib/utils"

export interface HoverMarqueeProps extends Omit<
  HTMLMotionProps<"div">,
  "children"
> {
  /** Content shown while the row is idle. */
  children: React.ReactNode
  /** Content repeated inside the moving preview track. */
  marquee: React.ReactNode
  /** Track speed in pixels per second. @default 96 */
  speed?: number
  /** Space between repeated preview items in pixels. @default 32 */
  gap?: number
  /** Move the preview track in the opposite direction. @default false */
  reverse?: boolean
  /** Force the preview open from outside the component. */
  active?: boolean
  /** Classes applied to the moving preview layer. */
  marqueeClassName?: string
}

/** Reveals a horizontally looping preview when a directory row is hovered. */
function HoverMarquee({
  children,
  marquee,
  speed = 96,
  gap = 32,
  reverse = false,
  active,
  className,
  marqueeClassName,
  onPointerEnter,
  onPointerLeave,
  onFocus,
  onBlur,
  ...props
}: HoverMarqueeProps) {
  const [hovered, setHovered] = React.useState(false)
  const [focused, setFocused] = React.useState(false)
  const reduceMotion = useReducedMotion()
  const revealed = active ?? (hovered || focused)

  return (
    <motion.div
      data-slot="hover-marquee"
      className={cn("relative overflow-hidden", className)}
      onPointerEnter={(event) => {
        setHovered(true)
        onPointerEnter?.(event)
      }}
      onPointerLeave={(event) => {
        setHovered(false)
        onPointerLeave?.(event)
      }}
      onFocus={(event) => {
        setFocused(true)
        onFocus?.(event)
      }}
      onBlur={(event) => {
        if (!event.currentTarget.contains(event.relatedTarget))
          setFocused(false)
        onBlur?.(event)
      }}
      {...props}
    >
      <motion.div
        data-slot="hover-marquee-idle"
        initial={false}
        animate={{ opacity: revealed ? 0 : 1, y: revealed ? -8 : 0 }}
        transition={{ duration: reduceMotion ? 0 : 0.2 }}
      >
        {children}
      </motion.div>

      <AnimatePresence initial={false}>
        {revealed ? (
          <motion.div
            data-slot="hover-marquee-preview"
            className={cn(
              "absolute inset-0 flex items-center",
              marqueeClassName
            )}
            initial={reduceMotion ? false : { opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 8 }}
            transition={{ duration: reduceMotion ? 0 : 0.24 }}
          >
            <InfiniteSlider
              aria-hidden="true"
              className="w-full"
              speed={reduceMotion ? 0 : speed}
              gap={gap}
              reverse={reverse}
            >
              {marquee}
            </InfiniteSlider>
          </motion.div>
        ) : null}
      </AnimatePresence>
    </motion.div>
  )
}

export { HoverMarquee }

属性 Props

属性类型默认值说明
childrenReact.ReactNode—在静止默认状态下展示的列表行或文本内容。
marqueeReact.ReactNode—悬停激活状态下在无限轨道中重复循环展示的内容单元。
speednumber96跑马灯滚动的线速度(像素 / 秒)。
gapnumber32循环项目之间的水平间距(像素)。
reversebooleanfalse是否反向滚动(从左向右)。
activeboolean—受控模式下显式强制保持跑马灯激活状态。
classNamestring—应用于外层容器的 CSS 类名。
marqueeClassNamestring—应用于悬停切入的跑马灯轨道的额外 CSS 类名(如修改背景色与文字对比度)。

事件 Events

属性类型默认值说明
onPointerEnter(event: React.PointerEvent) => void—光标移入行容器时触发。
onPointerLeave(event: React.PointerEvent) => void—光标移出行容器时触发。
onFocus(event: React.FocusEvent) => void—通过键盘 Tab 聚焦当前行时触发,激活跑马灯。
onBlur(event: React.FocusEvent) => void—失去键盘焦点时触发,恢复默认静止状态。

使用场景与设计规范

HoverMarquee 适用于创意设计作品集、特色服务列表或客户案例目录:

  • 信息密度切换:默认状态保持版面整洁低调,悬停时以高对比度(如黑底白字或主色填充)激发出丰富的关键词、技术栈或标语。
  • 正反方向交错:在多行列表中为奇偶行交替设置 reverse={index % 2 === 1},能创造出极具节奏感的视差动效。
  • 性能与适度:跑马灯由 requestAnimationFrame + translate3d 驱动,在不可见或未悬停时不会占用多余渲染开销。

场景示例

服务目录

在带边框的列表中使用主色预览层,悬停时展开该服务涵盖的能力标签,相邻行反向滚动避免视觉单调:

Loading…

团队与技能标签

在团队列表中悬停或聚焦某一行,展开该团队的技能标签:

Loading…

无障碍与交互 Accessibility

  • 键盘可访问性:为行容器赋予 tabIndex={0} 后,用户可以使用 Tab 键导航,聚焦时自动触发跑马灯展开。
  • 减弱动态效果降级:当用户开启 prefers-reduced-motion: reduce 时,跑马灯速度自动归零,且切换变为静态无位移淡入,保障前庭敏感用户舒适。
  • 无障碍树隔离:跑马灯循环层自带 aria-hidden="true",屏幕阅读器始终按序读取语义结构清晰的 children 静态默认内容。