组件
无限滑动 Infinite Slider
平滑循环内容的水平或垂直滑动轨道。
基础示例
Loading…
pnpm dlx wui@latest add @wui/infinite-slider"use client"
import * as React from "react"
import {
motion,
useAnimationFrame,
useInView,
useReducedMotion,
type HTMLMotionProps,
} from "motion/react"
import { cn } from "@/lib/utils"
export interface InfiniteSliderProps extends Omit<
HTMLMotionProps<"div">,
"children" | "direction"
> {
/** Items displayed in the continuously looping track. */
children: React.ReactNode
/** Space between items in pixels. @default 16 */
gap?: number
/** Track speed in pixels per second. @default 64 */
speed?: number
/** Track speed while hovered. Set to `0` to pause on hover. */
speedOnHover?: number
/** Slider axis. @default "horizontal" */
direction?: "horizontal" | "vertical"
/** Move in the opposite direction. @default false */
reverse?: boolean
}
/** A continuously looping horizontal or vertical content track. */
function InfiniteSlider({
children,
className,
gap = 16,
speed = 64,
speedOnHover,
direction = "horizontal",
reverse = false,
onMouseEnter,
onMouseLeave,
...props
}: InfiniteSliderProps) {
const containerRef = React.useRef<HTMLDivElement>(null)
const groupRef = React.useRef<HTMLDivElement>(null)
const progressRef = React.useRef(0)
const [groupSize, setGroupSize] = React.useState(0)
const [hovered, setHovered] = React.useState(false)
const reduceMotion = useReducedMotion()
const inView = useInView(containerRef)
React.useEffect(() => {
const group = groupRef.current
if (!group) return
const measure = () => {
const rect = group.getBoundingClientRect()
setGroupSize(direction === "horizontal" ? rect.width : rect.height)
}
measure()
const observer = new ResizeObserver(measure)
observer.observe(group)
return () => observer.disconnect()
}, [direction, gap, children])
useAnimationFrame((_, delta) => {
if (reduceMotion || !inView || groupSize === 0) return
const currentSpeed =
hovered && speedOnHover !== undefined ? speedOnHover : speed
progressRef.current =
(progressRef.current + (currentSpeed * delta) / 1000) % groupSize
const track = groupRef.current?.parentElement
if (!track) return
const progress = progressRef.current
const offset = reverse ? progress - groupSize : -progress
track.style.transform =
direction === "horizontal"
? `translate3d(${offset}px, 0, 0)`
: `translate3d(0, ${offset}px, 0)`
})
const groupClassName = cn(
"flex shrink-0",
direction === "horizontal" ? "flex-row" : "flex-col"
)
const groupStyle =
direction === "horizontal"
? { gap, paddingRight: gap }
: { gap, paddingBottom: gap }
return (
<motion.div
ref={containerRef}
data-slot="infinite-slider"
className={cn(
reduceMotion
? direction === "horizontal"
? "overflow-x-auto"
: "overflow-y-auto"
: "overflow-hidden",
className
)}
onMouseEnter={(event) => {
setHovered(true)
onMouseEnter?.(event)
}}
onMouseLeave={(event) => {
setHovered(false)
onMouseLeave?.(event)
}}
{...props}
>
<div
data-slot="infinite-slider-track"
className={cn(
"flex w-max will-change-transform",
direction === "vertical" && "flex-col"
)}
>
<div
ref={groupRef}
data-slot="infinite-slider-group"
className={groupClassName}
style={groupStyle}
>
{children}
</div>
{reduceMotion ? null : (
<div
aria-hidden="true"
inert
data-slot="infinite-slider-group"
className={groupClassName}
style={groupStyle}
>
{children}
</div>
)}
</div>
</motion.div>
)
}
export { InfiniteSlider }
组件作用
InfiniteSlider 用于品牌列表、合作伙伴、标签或轻量媒体带。它按像素速度持续移动,并复制一组内容形成无缝循环;减少动态效果时会停止并只保留一组内容。
组件属性
| Prop | Type | Default | Description |
|---|---|---|---|
| children * | ReactNode | — | Items displayed in the continuously looping track. |
| gap | number | 16 | Space between items in pixels. |
| speed | number | 64 | Track speed in pixels per second. |
| speedOnHover | number | — | Track speed while hovered. Set to `0` to pause on hover. |
| direction | "horizontal" | "vertical" | horizontal | Slider axis. |
| reverse | boolean | false | Move in the opposite direction. |
事件
组件透传容器的鼠标、指针与焦点事件。speedOnHover 用来改变悬停速度,设为 0 即可暂停。
扩展使用
垂直与反向
Loading…
使用纵向模式时必须给外层设置明确高度。内容应保持简短,且不要把需要键盘操作的控件放入会持续移动的轨道。