カルーセルとは

複数の画像やカードを横方向に切り替えて 1 枚ずつ表示する部品。

概要

複数の画像やカードを横一列に並べておき、前へ・次へボタンやドット(インジケーター)の操作で 1 枚ずつ切り替えて表示するパーツです。限られた面積で複数の情報を見せられるため、トップページの メインビジュアルや、商品画像の一覧などでよく使われます。

別名

表記ゆれ:カルーセル、スライダー、スライドショー、画像スライダー、carousel

「スライダー」は入力用の「レンジスライダー」と紛らわしいことがあるため、区別したいときは 「画像スライダー」「カルーセル」と呼ぶと伝わりやすくなります。

AI への伝え方

「カルーセルを実装して」で通じます。自動再生するかどうかは仕様が分かれる点なので、 自動再生させたい場合は「自動再生も付けて」と明示してください(自動再生は事故りやすいので、 指示がない場合は付けない実装が安全です)。何枚目を表示しているかはスクリーンリーダーにも 伝わるよう、aria-live で現在枚数を通知する実装にすると親切です。

似たパーツとの違い

サムネイルを格子状に並べる「画像ギャラリー」は、複数の画像を同時に一覧できる点がカルーセルと異なります。 カルーセルは 1 枚ずつしか見えません。タブと同様に「切り替えて 1 つだけ表示する」という構造は共通していますが、 「タブ」は見出しを選んで内容を切り替えるのに対し、カルーセルは前へ・次への操作で順番に送る点が異なります。

デモ

コード

HTML
<div class="carousel-demo" role="group" aria-label="スライドショー">
  <div class="carousel-demo__viewport">
    <ul class="carousel-demo__track">
      <li class="carousel-demo__slide carousel-demo__slide--1" aria-hidden="false">スライド 1</li>
      <li class="carousel-demo__slide carousel-demo__slide--2" aria-hidden="true">スライド 2</li>
      <li class="carousel-demo__slide carousel-demo__slide--3" aria-hidden="true">スライド 3</li>
    </ul>
  </div>

  <button class="carousel-demo__nav carousel-demo__nav--prev" type="button" aria-label="前のスライド">‹</button>
  <button class="carousel-demo__nav carousel-demo__nav--next" type="button" aria-label="次のスライド">›</button>

  <div class="carousel-demo__dots" role="tablist" aria-label="スライド切り替え">
    <button class="carousel-demo__dot" type="button" role="tab" aria-selected="true" aria-label="1 枚目"></button>
    <button class="carousel-demo__dot" type="button" role="tab" aria-selected="false" aria-label="2 枚目"></button>
    <button class="carousel-demo__dot" type="button" role="tab" aria-selected="false" aria-label="3 枚目"></button>
  </div>

  <p class="carousel-demo__status" aria-live="polite">1 / 3 枚目</p>
</div>
CSS
.carousel-demo {
  position: relative;
  width: 320px;
  font-family: sans-serif;
}

.carousel-demo__viewport {
  overflow: hidden;
  border-radius: 12px;
}

.carousel-demo__track {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
  transition: transform 0.3s ease;
  /* JS が translateX(-n * 100%) で現在位置を更新する */
  transform: translateX(0%);
}

.carousel-demo__slide {
  flex: 0 0 100%;
  height: 160px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 16px;
}

.carousel-demo__slide--1 {
  background: #0017c1;
}

.carousel-demo__slide--2 {
  background: #666666;
}

.carousel-demo__slide--3 {
  background: #333333;
}

.carousel-demo__nav {
  position: absolute;
  top: 80px;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.9);
  color: #212121;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}

.carousel-demo__nav:hover {
  background: #fff;
}

.carousel-demo__nav--prev {
  left: 8px;
}

.carousel-demo__nav--next {
  right: 8px;
}

.carousel-demo__dots {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 12px;
}

.carousel-demo__dot {
  width: 8px;
  height: 8px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #b3b3b3;
  cursor: pointer;
}

.carousel-demo__dot[aria-selected="true"] {
  background: #0017c1;
}

.carousel-demo__status {
  margin: 8px 0 0;
  font-size: 12px;
  color: #666;
  text-align: center;
}
JS
const track = document.querySelector(".carousel-demo__track");
const slides = document.querySelectorAll(".carousel-demo__slide");
const dots = document.querySelectorAll(".carousel-demo__dot");
const status = document.querySelector(".carousel-demo__status");
const prevButton = document.querySelector(".carousel-demo__nav--prev");
const nextButton = document.querySelector(".carousel-demo__nav--next");

let current = 0;

// 自動再生は行わない(自動再生が必要な場合は指示で追加する)
function update() {
  track.style.transform = `translateX(-${current * 100}%)`;
  slides.forEach((slide, index) => {
    slide.setAttribute("aria-hidden", String(index !== current));
  });
  dots.forEach((dot, index) => {
    dot.setAttribute("aria-selected", String(index === current));
  });
  status.textContent = `${current + 1} / ${slides.length} 枚目`;
}

prevButton.addEventListener("click", () => {
  current = (current - 1 + slides.length) % slides.length;
  update();
});

nextButton.addEventListener("click", () => {
  current = (current + 1) % slides.length;
  update();
});

dots.forEach((dot, index) => {
  dot.addEventListener("click", () => {
    current = index;
    update();
  });
});