Next.js中如何实现横向与纵向双向无限滚动?
在Next.js中实现横向/纵向无限滚动图片轮播容器
无限滚动轮播的核心逻辑是监听容器滚动事件,当滚动接近边缘时,复制首尾内容插入到容器的另一侧,同时重置滚动位置,让用户感知不到内容重复,从而营造无限循环的效果。以下是具体实现方案:
核心实现思路
- 用
useRef获取滚动容器和内容容器的DOM节点 - 监听滚动事件,判断滚动位置是否接近边缘
- 复制首尾内容插入到对应位置,同时调整滚动偏移量,隐藏内容重复的痕迹
- 加入防抖处理,避免滚动事件触发过于频繁导致性能问题
横向无限滚动实现代码
import { useEffect, useRef, useState } from 'react'; const HorizontalInfiniteCarousel = ({ images }) => { const carouselRef = useRef(null); const contentRef = useRef(null); const [isScrolling, setIsScrolling] = useState(false); useEffect(() => { const carousel = carouselRef.current; const content = contentRef.current; const handleScroll = () => { if (isScrolling) return; setIsScrolling(true); // 滚动到左侧边缘 if (carousel.scrollLeft <= 50) { const lastImageGroup = content.lastElementChild.cloneNode(true); content.insertBefore(lastImageGroup, content.firstChild); carousel.scrollLeft += lastImageGroup.offsetWidth; } // 滚动到右侧边缘 else if (carousel.scrollLeft >= carousel.scrollWidth - carousel.offsetWidth - 50) { const firstImageGroup = content.firstElementChild.cloneNode(true); content.appendChild(firstImageGroup); carousel.scrollLeft -= firstImageGroup.offsetWidth; } setTimeout(() => setIsScrolling(false), 100); }; carousel.addEventListener('scroll', handleScroll); return () => carousel.removeEventListener('scroll', handleScroll); }, [isScrolling]); // 初始复制首尾内容,避免首次滚动出现空白 useEffect(() => { const content = contentRef.current; const firstGroup = content.firstElementChild.cloneNode(true); const lastGroup = content.lastElementChild.cloneNode(true); content.appendChild(firstGroup); content.insertBefore(lastGroup, content.firstChild); }, [images]); return ( <div ref={carouselRef} className="overflow-x-auto scrollbar-hide" style={{ width: '100%', maxWidth: '1200px', margin: '0 auto' }} > <div ref={contentRef} className="flex"> {images.map((img, idx) => ( <div key={idx} className="inline-block w-64 h-48 mx-2"> <img src={img.src} alt={img.alt} className="w-full h-full object-cover rounded-lg" /> </div> ))} </div> </div> ); }; export default HorizontalInfiniteCarousel;
配套隐藏滚动条的CSS:
.scrollbar-hide::-webkit-scrollbar { display: none; } .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
纵向无限滚动实现代码
import { useEffect, useRef, useState } from 'react'; const VerticalInfiniteCarousel = ({ images }) => { const carouselRef = useRef(null); const contentRef = useRef(null); const [isScrolling, setIsScrolling] = useState(false); useEffect(() => { const carousel = carouselRef.current; const content = contentRef.current; const handleScroll = () => { if (isScrolling) return; setIsScrolling(true); // 滚动到顶部边缘 if (carousel.scrollTop <= 50) { const lastImageGroup = content.lastElementChild.cloneNode(true); content.insertBefore(lastImageGroup, content.firstChild); carousel.scrollTop += lastImageGroup.offsetHeight; } // 滚动到底部边缘 else if (carousel.scrollTop >= carousel.scrollHeight - carousel.offsetHeight - 50) { const firstImageGroup = content.firstElementChild.cloneNode(true); content.appendChild(firstImageGroup); carousel.scrollTop -= firstImageGroup.offsetHeight; } setTimeout(() => setIsScrolling(false), 100); }; carousel.addEventListener('scroll', handleScroll); return () => carousel.removeEventListener('scroll', handleScroll); }, [isScrolling]); useEffect(() => { const content = contentRef.current; const firstGroup = content.firstElementChild.cloneNode(true); const lastGroup = content.lastElementChild.cloneNode(true); content.appendChild(firstGroup); content.insertBefore(lastGroup, content.firstChild); }, [images]); return ( <div ref={carouselRef} className="overflow-y-auto scrollbar-hide" style={{ height: '600px', width: '300px', margin: '0 auto' }} > <div ref={contentRef}> {images.map((img, idx) => ( <div key={idx} className="mb-2"> <img src={img.src} alt={img.alt} className="w-full h-48 object-cover rounded-lg" /> </div> ))} </div> </div> ); }; export default VerticalInfiniteCarousel;
横向+纵向网格无限滚动实现
如果需要同时支持横竖方向的无限滚动,可结合上述两种逻辑,分别处理横向和纵向的滚动边缘:
import { useEffect, useRef, useState } from 'react'; const GridInfiniteCarousel = ({ images, cols = 3 }) => { const carouselRef = useRef(null); const contentRef = useRef(null); const [isScrolling, setIsScrolling] = useState(false); useEffect(() => { const carousel = carouselRef.current; const content = contentRef.current; const handleScroll = () => { if (isScrolling) return; setIsScrolling(true); // 横向滚动处理 if (carousel.scrollLeft <= 50) { const lastCol = images.slice(images.length - cols); const colGroup = document.createElement('div'); colGroup.className = 'grid grid-cols-1 gap-2'; lastCol.forEach(img => { const imgEl = document.createElement('div'); imgEl.className = 'w-full h-48'; const imgTag = document.createElement('img'); imgTag.src = img.src; imgTag.alt = img.alt; imgTag.className = 'w-full h-full object-cover rounded-lg'; imgEl.appendChild(imgTag); colGroup.appendChild(imgEl); }); content.insertBefore(colGroup, content.firstChild); carousel.scrollLeft += colGroup.offsetWidth; } else if (carousel.scrollLeft >= carousel.scrollWidth - carousel.offsetWidth - 50) { const firstCol = images.slice(0, cols); const colGroup = document.createElement('div'); colGroup.className = 'grid grid-cols-1 gap-2'; firstCol.forEach(img => { const imgEl = document.createElement('div'); imgEl.className = 'w-full h-48'; const imgTag = document.createElement('img'); imgTag.src = img.src; imgTag.alt = img.alt; imgTag.className = 'w-full h-full object-cover rounded-lg'; imgEl.appendChild(imgTag); colGroup.appendChild(imgEl); }); content.appendChild(colGroup); carousel.scrollLeft -= colGroup.offsetWidth; } // 纵向滚动处理 if (carousel.scrollTop <= 50) { const lastRow = images.slice(images.length - cols); const rowGroup = document.createElement('div'); rowGroup.className = 'flex gap-2'; lastRow.forEach(img => { const imgEl = document.createElement('div'); imgEl.className = 'w-full h-48'; const imgTag = document.createElement('img'); imgTag.src = img.src; imgTag.alt = img.alt; imgTag.className = 'w-full h-full object-cover rounded-lg'; imgEl.appendChild(imgTag); rowGroup.appendChild(imgEl); }); content.insertBefore(rowGroup, content.firstChild); carousel.scrollTop += rowGroup.offsetHeight; } else if (carousel.scrollTop >= carousel.scrollHeight - carousel.offsetHeight - 50) { const firstRow = images.slice(0, cols); const rowGroup = document.createElement('div'); rowGroup.className = 'flex gap-2'; firstRow.forEach(img => { const imgEl = document.createElement('div'); imgEl.className = 'w-full h-48'; const imgTag = document.createElement('img'); imgTag.src = img.src; imgTag.alt = img.alt; imgTag.className = 'w-full h-full object-cover rounded-lg'; imgEl.appendChild(imgTag); rowGroup.appendChild(imgEl); }); content.appendChild(rowGroup); carousel.scrollTop -= rowGroup.offsetHeight; } setTimeout(() => setIsScrolling(false), 150); }; carousel.addEventListener('scroll', handleScroll); return () => carousel.removeEventListener('scroll', handleScroll); }, [isScrolling, images, cols]); // 初始复制首尾内容 useEffect(() => { const content = contentRef.current; // 横向复制首尾列 const lastCol = images.slice(images.length - cols); const colGroup = document.createElement('div'); colGroup.className = 'grid grid-cols-1 gap-2'; lastCol.forEach(img => { const imgEl = document.createElement('div'); imgEl.className = 'w-full h-48'; const imgTag = document.createElement('img'); imgTag.src = img.src; imgTag.alt = img.alt; imgTag.className = 'w-full h-full object-cover rounded-lg'; imgEl.appendChild(imgTag); colGroup.appendChild(imgEl); }); content.insertBefore(colGroup, content.firstChild); const firstCol = images.slice(0, cols); const firstColGroup = document.createElement('div'); firstColGroup.className = 'grid grid-cols-1 gap-2'; firstCol.forEach(img => { const imgEl = document.createElement('div'); imgEl.className = 'w-full h-48'; const imgTag = document.createElement('img'); imgTag.src = img.src; imgTag.alt = img.alt; imgTag.className = 'w-full h-full object-cover rounded-lg'; imgEl.appendChild(imgTag); firstColGroup.appendChild(imgEl); }); content.appendChild(firstColGroup); // 纵向复制首尾行 const lastRow = images.slice(images.length - cols); const rowGroup = document.createElement('div'); rowGroup.className = 'flex gap-2'; lastRow.forEach(img => { const imgEl = document.createElement('div'); imgEl.className = 'w-full h-48'; const imgTag = document.createElement('img'); imgTag.src = img.src; imgTag.alt = img.alt; imgTag.className = 'w-full h-full object-cover rounded-lg'; imgEl.appendChild(imgTag); rowGroup.appendChild(imgEl); }); content.insertBefore(rowGroup, content.firstChild); const firstRow = images.slice(0, cols); const firstRowGroup = document.createElement('div'); firstRowGroup.className = 'flex gap-2'; firstRow.forEach(img => { const imgEl = document.createElement('div'); imgEl.className = 'w-full h-48'; const imgTag = document.createElement('img'); imgTag.src = img.src; imgTag.alt = img.alt; imgTag.className = 'w-full h-full object-cover rounded-lg'; imgEl.appendChild(imgTag); firstRowGroup.appendChild(imgEl); }); content.appendChild(firstRowGroup); }, [images, cols]); return ( <div ref={carouselRef} className="overflow-auto scrollbar-hide" style={{ width: '800px', height: '600px', margin: '0 auto' }} > <div ref={contentRef} className="grid grid-cols-3 gap-2"> {images.map((img, idx) => ( <div key={idx} className="w-full h-48"> <img src={img.src} alt={img.alt} className="w-full h-full object-cover rounded-lg" /> </div> ))} </div> </div> ); }; export default GridInfiniteCarousel;
关键注意事项
- 防抖处理:通过
isScrolling状态和定时器限制滚动事件的处理频率,避免DOM操作过于频繁 - 滚动阈值:设置50px的触发阈值,提前复制内容,防止用户看到滚动边缘的空白
- DOM复用:使用
cloneNode复用已有DOM节点,减少内存开销 - 初始内容填充:首次渲染时复制首尾内容,保证初始滚动时不会出现空白
内容的提问来源于stack exchange,提问作者Ajayaditya
相关产品推荐
相关产品推荐

