
本文介绍一种在 react 中结合 css 动画与 javascript 交互控制的水平滚动横幅方案,既能实现平滑自动滚动,又支持鼠标滚轮/触摸板双向手动拖拽,且避免内容丢失问题。
本文介绍一种在 react 中结合 css 动画与 javascript 交互控制的水平滚动横幅方案,既能实现平滑自动滚动,又支持鼠标滚轮/触摸板双向手动拖拽,且避免内容丢失问题。
在构建响应式横幅(如产品展示、新闻轮播、广告位)时,常见的需求是:默认自动循环滚动,用户悬停时暂停,移出后恢复;同时允许通过滚轮或拖拽进行任意方向的手动浏览。但若直接混合 transform: translateX() 的 CSS 动画与 scrollLeft 手动控制,极易导致视觉错位——因为 translateX 改变的是元素自身坐标系,而 scrollLeft 操作的是容器的滚动偏移,二者状态不统一,造成“内容滚出去就回不来”的现象。
✅ 推荐方案:统一使用 scrollLeft 驱动,CSS 动画退场
为确保状态一致性,应放弃纯 CSS @keyframes + transform 方案,改用 JavaScript 控制 scrollLeft 实现全自动滚动,再叠加手动交互逻辑。这样所有滚动行为(自动、滚轮、拖拽)都作用于同一 DOM 属性,天然支持双向、无缝衔接。
以下是一个完整、可复用的 React 横幅组件示例(基于函数组件 + useRef + useEffect + useLayoutEffect):
import { useState, useRef, useEffect, useLayoutEffect } from 'react';
const HorizontalMarquee = ({ children }: { children: React.ReactNode }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [isPaused, setIsPaused] = useState(false);
const animationFrameRef = useRef<number>(0);
const scrollPositionRef = useRef<number>(0);
const scrollSpeed = 1.2; // 像素/帧,可调
// 自动滚动主逻辑(requestAnimationFrame 驱动)
const animateScroll = () => {
if (!containerRef.current || isPaused) return;
const container = containerRef.current;
const maxScroll = container.scrollWidth - container.clientWidth;
// 循环滚动:到达右边界后跳回起点(视觉无缝)
scrollPositionRef.current += scrollSpeed;
if (scrollPositionRef.current >= maxScroll) {
scrollPositionRef.current = 0;
}
container.scrollLeft = scrollPositionRef.current;
animationFrameRef.current = requestAnimationFrame(animateScroll);
};
// 启动/停止动画
useEffect(() => {
if (!isPaused) {
animationFrameRef.current = requestAnimationFrame(animateScroll);
} else {
cancelAnimationFrame(animationFrameRef.current);
}
return () => cancelAnimationFrame(animationFrameRef.current);
}, [isPaused]);
// 手动滚轮控制(双向)
const handleWheel = (e: WheelEvent) => {
e.preventDefault();
if (!containerRef.current) return;
const container = containerRef.current;
container.scrollLeft += e.deltaY > 0 ? 30 : -30; // 向下滚→右移,向上滚→左移
};
// 悬停暂停/恢复
const handleMouseEnter = () => setIsPaused(true);
const handleMouseLeave = () => setIsPaused(false);
return (
<div
ref={containerRef}
className="marquee-container"
onWheel={handleWheel}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{
overflowX: 'auto',
scrollBehavior: 'smooth', // 可选:启用原生平滑滚动(注意兼容性)
WebkitOverflowScrolling: 'touch', // iOS 滚动优化
}}
>
<div className="marquee-content">
{children}
</div>
</div>
);
};
export default HorizontalMarquee;配套 CSS(关键点:禁用默认换行,确保子元素水平排列):
.marquee-container {
display: flex;
overflow-x: auto;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
/* 隐藏滚动条(可选) */
scrollbar-width: none; /* Firefox */
}
.marquee-container::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
.marquee-content {
display: flex;
flex-wrap: nowrap;
gap: 1rem;
padding: 0.5rem 0;
}
/* 子项需设为 flex-shrink: 0 防止被压缩 */
.marquee-content > * {
flex-shrink: 0;
min-width: 200px; /* 根据实际内容调整 */
}⚠️ 注意事项与最佳实践
- 性能优先:使用 requestAnimationFrame 替代 setInterval,避免丢帧;useLayoutEffect 仅在需要同步读取布局(如首次计算 scrollWidth)时使用,本例中 useEffect 已足够。
- 移动端适配:添加 touch-action: pan-x 到容器可提升触摸板/手机滑动体验;若需支持拖拽,可监听 pointerdown/pointermove 事件(本例未展开,但原理一致)。
- 无障碍友好:保留原生 scrollLeft 行为,天然支持键盘 Tab 导航与 Space/Arrow 键滚动;如需增强,可添加 aria-live 或 role="region"。
- 无限循环优化:当前示例采用“重置到 0”方式实现循环,若内容较短易察觉跳变,可考虑双倍内容拼接(即渲染两份 children),并动态维护 scrollLeft 在 [0, originalWidth] 区间内,视觉更平滑(进阶技巧)。
✅ 总结
与其强行混合 transform 动画与 scrollLeft 手动控制(导致状态冲突),不如以 scrollLeft 为唯一真相源,用 JS 统一驱动自动滚动,并开放滚轮/悬停等交互接口。该方案逻辑清晰、调试友好、兼容性强,且易于扩展拖拽、点击跳转、进度指示等功能,是现代 React 横幅组件的稳健选择。










