Nextjs/React中如何实现单图横向拖拽滚动自定义水平滚动条
Next.js+React+TS+Tailwind 实现可横向拖拽浏览的Hero单图组件
核心实现思路
- 外层做固定视口大小的滚动容器,隐藏原生滚动条,开启横向溢出滚动
- 内层放置超宽Hero图,宽度按图片原始比例设置,保证高度铺满视口不变形
- 监听鼠标、触摸事件,记录拖拽起始位置,实时同步容器横向滚动偏移
- 增加拖拽光标切换、滚动惯性、默认交互禁止的细节处理,保证拖拽跟手度
- 拖拽状态用ref存储而非state,避免频繁重渲染导致拖拽卡顿
完整组件代码
'use client' // Next.js App Router 模式需添加,标记为客户端组件;Pages Router可删除 import Image from 'next/image' import { useRef, useEffect, useState } from 'react' export default function DraggableHero() { const scrollContainerRef = useRef<HTMLDivElement>(null) // 拖拽状态用ref存储,避免频繁触发重渲染 const isDragging = useRef(false) const startX = useRef(0) const startScrollLeft = useRef(0) const lastX = useRef(0) const velocity = useRef(0) // 仅用于切换光标样式的状态 const [isDraggingActive, setIsDraggingActive] = useState(false) useEffect(() => { const container = scrollContainerRef.current if (!container) return // 拖拽开始:初始化状态 const handleDragStart = (e: MouseEvent | TouchEvent) => { isDragging.current = true setIsDraggingActive(true) const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX startX.current = clientX lastX.current = clientX startScrollLeft.current = container.scrollLeft velocity.current = 0 container.style.cursor = 'grabbing' } // 拖拽移动:同步滚动位置 const handleDragMove = (e: MouseEvent | TouchEvent) => { if (!isDragging.current) return e.preventDefault() const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX const deltaX = clientX - startX.current container.scrollLeft = startScrollLeft.current - deltaX // 记录移动速度用于惯性计算 velocity.current = clientX - lastX.current lastX.current = clientX } // 拖拽结束:执行惯性动画 const handleDragEnd = () => { if (!isDragging.current) return isDragging.current = false setIsDraggingActive(false) container.style.cursor = 'grab' // 惯性衰减动画 const runInertia = () => { if (Math.abs(velocity.current) < 0.5) return velocity.current *= 0.95 // 摩擦系数,越小停顿越快 container.scrollLeft -= velocity.current requestAnimationFrame(runInertia) } requestAnimationFrame(runInertia) } // 绑定事件,事件挂载到window避免拖拽出容器区域失效 container.addEventListener('mousedown', handleDragStart) container.addEventListener('touchstart', handleDragStart, { passive: true }) window.addEventListener('mousemove', handleDragMove) window.addEventListener('touchmove', handleDragMove, { passive: false }) window.addEventListener('mouseup', handleDragEnd) window.addEventListener('touchend', handleDragEnd) return () => { container.removeEventListener('mousedown', handleDragStart) container.removeEventListener('touchstart', handleDragStart) window.removeEventListener('mousemove', handleDragMove) window.removeEventListener('touchmove', handleDragMove) window.removeEventListener('mouseup', handleDragEnd) window.removeEventListener('touchend', handleDragEnd) } }, []) return ( <section className="w-full h-screen relative overflow-hidden"> <div ref={scrollContainerRef} className={`w-full h-full overflow-x-scroll overflow-y-hidden scrollbar-hide select-none cursor-grab ${isDraggingActive ? 'cursor-grabbing' : ''}`} draggable={false} > {/* 内层图片容器,宽度可根据图片实际比例调整 */} <div className="relative h-full w-[250vw]"> <Image src="/your-hero-wide-image.jpg" // 替换为项目内实际超宽Hero图路径 alt="Hero banner" fill draggable={false} className="object-cover object-left pointer-events-none" priority /> </div> </div> {/* 可选:添加拖拽提示文案 */} <div className="absolute bottom-8 left-1/2 -translate-x-1/2 text-white/80 text-sm pointer-events-none"> 拖拽浏览完整图片 </div> </section> ) }
Tailwind 配置补充
在项目全局CSS文件中添加隐藏滚动条的自定义工具类,无需引入额外插件:
@tailwind base; @tailwind components; @tailwind utilities; @layer utilities { .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; } .scrollbar-hide::-webkit-scrollbar { display: none; } }
可调优化项
- 内层图片容器不要硬编码固定宽度,可根据图片原始宽高比动态计算:比如图片原始分辨率为3000*1080,容器高度为100vh时,宽度设置为
calc(100vh * (3000 / 1080)),可保证图片高度刚好铺满视口,无拉伸变形 - 惯性动画的摩擦系数可按需调整,数值越大惯性滑动距离越长,数值越小停顿越快
- 若需要支持鼠标滚轮横向滚动,可额外监听容器
wheel事件,将纵向滚动的delta值转换为横向滚动偏移 - 移动端如果需要更贴近原生的滚动手感,可移除自定义惯性逻辑,直接使用浏览器原生滚动的惯性效果,只保留拖拽同步的核心逻辑即可
内容的提问来源于stack exchange,提问作者Enadot
相关产品推荐
相关产品推荐

