源本科技 | 码上会

响应式轮播图

2026/01/12
15
0

学习目标

  • 掌握使用 Flex + transform: translateX 实现水平滑动轮播的核心原理

  • 学会用 原生 JavaScript 控制 DOM 状态与动画

  • 实现 手动导航(Prev/Next)自动播放(Auto-play) 双模式

  • 理解 索引边界处理循环切换逻辑

  • 构建 响应式、无障碍友好 的 UI 组件


轮播图是什么

轮播图(Carousel / Slider) 是一种常见的 Web 组件,用于在有限空间内循环展示多张图片或内容卡片。广泛应用于:

  • 首页横幅广告

  • 产品展示

  • 用户评价轮播

  • 新闻头条滚动


核心设计思路

方法

原理

优点

缺点

display: none/block 切换

每次只显示一张图

简单

无过渡动画

opacity 淡入淡出

透明度变化

视觉柔和

多图重叠需绝对定位

transform: translateX 滑动

整体容器平移

流畅、性能好、支持手势扩展

需精确计算位移

我们选择 第三种方案 —— 通过移动 .img-wrap 容器实现无缝滑动


HTML 结构搭建

<div class="carousel">
  <div class="image-wrapper">
    <img src="image1.jpg" alt="Slide 1">
    <img src="image2.jpg" alt="Slide 2">
    <img src="image3.jpg" alt="Slide 3">
  </div>
  <button class="nav-btn prev">‹</button>
  <button class="nav-btn next">›</button>
</div>

关键点说明:

  • .carousel:外层容器,设置 overflow: hidden 隐藏超出部分

  • .image-wrapper所有图片的父容器,使用 display: flex 横向排列

  • <img>:每张图片宽度为 100%,高度自适应

  • .nav-btn:导航按钮,使用语义化 <button> 提升无障碍访问(a11y)

重要:所有图片必须具有相同宽高比,否则滑动时会出现布局跳动。


CSS 样式实现

body {
  margin: 0;
  font-family: Arial, sans-serif;
  background: #f4f4f4;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
}

.carousel {
  position: relative;
  width: 80%;
  max-width: 600px;
  overflow: hidden;
  border-radius: 10px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.image-wrapper {
  display: flex;
  transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.image-wrapper img {
  width: 100%;
  height: auto;
  flex-shrink: 0; /* 防止图片被压缩 */
}

.nav-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.6);
  color: white;
  border: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  font-size: 18px;
  cursor: pointer;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
}

.nav-btn:hover {
  background: rgba(0, 0, 0, 0.85);
}

.prev { left: 15px; }
.next { right: 15px; }

设计亮点:

  • flex-shrink: 0:确保每张图片保持原始宽度,不被 flex 压缩

  • cubic-bezier 过渡:比 ease 更自然的滑动曲线

  • 圆形按钮 + 居中图标:现代 UI 风格

  • z-index 保证按钮在图片上方


JavaScript 交互逻辑

// 获取 DOM 元素
const prevBtn = document.querySelector('.prev');
const nextBtn = document.querySelector('.next');
const imageWrapper = document.querySelector('.image-wrapper');
const images = document.querySelectorAll('.image-wrapper img');

let currentIndex = 0;
const totalImages = images.length;

// 更新轮播位置
function updateCarousel() {
  // 边界处理:循环切换
  if (currentIndex >= totalImages) currentIndex = 0;
  if (currentIndex < 0) currentIndex = totalImages - 1;

  // 计算位移:每张图占 100%,第 n 张图位移 -n * 100%
  imageWrapper.style.transform = `translateX(-${currentIndex * 100}%)`;
}

// 下一张
nextBtn.addEventListener('click', () => {
  currentIndex++;
  updateCarousel();
});

// 上一张
prevBtn.addEventListener('click', () => {
  currentIndex--;
  updateCarousel();
});

// 自动播放(每 3 秒切换)
let autoPlayInterval = setInterval(() => {
  currentIndex++;
  updateCarousel();
}, 3000);

// 可选:鼠标悬停暂停自动播放
const carouselContainer = document.querySelector('.carousel');
carouselContainer.addEventListener('mouseenter', () => {
  clearInterval(autoPlayInterval);
});
carouselContainer.addEventListener('mouseleave', () => {
  autoPlayInterval = setInterval(() => {
    currentIndex++;
    updateCarousel();
  }, 3000);
});

核心机制

功能

实现方式

滑动动画

修改 transform: translateX(-N%),CSS transition 自动补间

索引管理

currentIndex 记录当前显示的图片序号(从 0 开始)

循环切换

currentIndex >= length 时重置为 0,实现“无缝循环”

自动播放

setInterval 定时触发切换,clearInterval 可暂停

用户体验优化

鼠标悬停暂停自动播放,避免干扰用户操作


完整代码整合

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>轮播图</title>
  <style>
    body {
      margin: 0;
      font-family: Arial, sans-serif;
      background: #f4f4f4;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
    }
    .carousel {
      position: relative;
      width: 80%;
      max-width: 600px;
      overflow: hidden;
      border-radius: 10px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    }
    .image-wrapper {
      display: flex;
      transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    }
    .image-wrapper img {
      width: 100%;
      height: auto;
      flex-shrink: 0;
    }
    .nav-btn {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      background: rgba(0, 0, 0, 0.6);
      color: white;
      border: none;
      width: 40px;
      height: 40px;
      border-radius: 50%;
      font-size: 18px;
      cursor: pointer;
      z-index: 10;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .nav-btn:hover {
      background: rgba(0, 0, 0, 0.85);
    }
    .prev { left: 15px; }
    .next { right: 15px; }
  </style>
</head>
<body>
  <div class="carousel">
    <div class="image-wrapper">
      <img src="https://img1.baidu.com/it/u=36078727,2257383763&fm=253&fmt=auto&app=120&f=JPEG?w=667&h=500" alt="卡提西亚">
      <img src="https://img1.baidu.com/it/u=3701525992,1836708913&fm=253&fmt=auto&app=138&f=JPEG?w=888&h=500" alt="夏空">
      <img src="https://img1.baidu.com/it/u=2879016026,2518577144&fm=253&fmt=auto&app=120&f=JPEG?w=667&h=500" alt="守岸人">
    </div>
    <button class="nav-btn prev">‹</button>
    <button class="nav-btn next">›</button>
  </div>

  <script>
    const prevBtn = document.querySelector('.prev');
    const nextBtn = document.querySelector('.next');
    const imageWrapper = document.querySelector('.image-wrapper');
    const images = document.querySelectorAll('.image-wrapper img');
    let currentIndex = 0;
    const totalImages = images.length;

    function updateCarousel() {
      if (currentIndex >= totalImages) currentIndex = 0;
      if (currentIndex < 0) currentIndex = totalImages - 1;
      imageWrapper.style.transform = `translateX(-${currentIndex * 100}%)`;
    }

    nextBtn.addEventListener('click', () => {
      currentIndex++;
      updateCarousel();
    });

    prevBtn.addEventListener('click', () => {
      currentIndex--;
      updateCarousel();
    });

    // 自动播放
    let autoPlayInterval = setInterval(() => {
      currentIndex++;
      updateCarousel();
    }, 3000);

    // 悬停暂停
    const carousel = document.querySelector('.carousel');
    carousel.addEventListener('mouseenter', () => clearInterval(autoPlayInterval));
    carousel.addEventListener('mouseleave', () => {
      autoPlayInterval = setInterval(() => {
        currentIndex++;
        updateCarousel();
      }, 3000);
    });
  </script>
</body>
</html>

进阶功能

  1. 指示器(Dots):底部小圆点显示当前页码

  2. 键盘导航:支持 ← → 方向键控制

  3. 触摸滑动:监听 touchstart/touchmove/touchend 实现移动端手势

  4. 懒加载:仅加载可视区域图片,提升性能

  5. 响应式断点:在小屏设备上禁用自动播放

// 示例:添加指示器
// HTML: <div class="dots"></div>
// JS:
// images.forEach((_, i) => {
//   const dot = document.createElement('span');
//   dot.classList.add('dot');
//   dot.addEventListener('click', () => { currentIndex = i; updateCarousel(); });
//   document.querySelector('.dots').appendChild(dot);
// });

重点总结

  • 核心原理transform: translateX + flex 布局实现平滑滑动

  • 状态管理:用 currentIndex 控制当前显示项

  • 循环逻辑:通过模运算或条件判断实现首尾衔接

  • 用户体验:自动播放 + 悬停暂停 + 清晰导航

  • 无障碍基础:使用 <button>alt 属性


思考题

  1. 当前实现中,如果用户快速连续点击 “Next” 按钮,会发生什么?如何防止动画中断或错乱?

  2. 如何修改代码以支持垂直轮播(上下滑动)?

  3. 如果图片数量动态变化(如通过 API 加载),如何让轮播图自动适配?