You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 17:16:00