组件
悬停跑马灯 Hover Marquee
当列表行被悬停或聚焦时,原地替换为水平无限循环滚动的跑马灯轨道预览。
基础用法
鼠标悬停在列表项上,静态标题将上移淡出,并原地切入水平无缝循环滚动的跑马灯轨道:
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"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
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| children | React.ReactNode | — | 在静止默认状态下展示的列表行或文本内容。 |
| marquee | React.ReactNode | — | 悬停激活状态下在无限轨道中重复循环展示的内容单元。 |
| speed | number | 96 | 跑马灯滚动的线速度(像素 / 秒)。 |
| gap | number | 32 | 循环项目之间的水平间距(像素)。 |
| reverse | boolean | false | 是否反向滚动(从左向右)。 |
| active | boolean | — | 受控模式下显式强制保持跑马灯激活状态。 |
| className | string | — | 应用于外层容器的 CSS 类名。 |
| marqueeClassName | string | — | 应用于悬停切入的跑马灯轨道的额外 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静态默认内容。