カルーセル / スライダー

自動再生・矢印・ドット・スワイプに対応した軽量カルーセル。translateXによる横移動で、バナーや特集の回遊表示に使えます。

#javascript#css#animation#touch

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW:季節のおすすめを回遊させるカルーセル -->
<div class="cafe">
  <div class="cafe__bar">
    <span class="cafe__logo">☕ MOON BREW</span>
    <span class="cafe__tag">SEASONAL</span>
  </div>

  <div class="carousel" data-carousel>
    <div class="carousel__viewport">
      <div class="carousel__track">
        <div class="slide" style="background-image:url('https://picsum.photos/640/300?random=11')">
          <div class="slide__copy">
            <span class="slide__pill">秋限定</span>
            <h2 class="slide__title">焙煎マロンラテ</h2>
            <p class="slide__sub">深煎り豆と国産栗の甘み。湯気まで香ばしい一杯。</p>
          </div>
        </div>
        <div class="slide" style="background-image:url('https://picsum.photos/640/300?random=12')">
          <div class="slide__copy">
            <span class="slide__pill">新発売</span>
            <h2 class="slide__title">水出しコールドブリュー</h2>
            <p class="slide__sub">18時間かけて低温抽出。すっきり澄んだ後味。</p>
          </div>
        </div>
        <div class="slide" style="background-image:url('https://picsum.photos/640/300?random=13')">
          <div class="slide__copy">
            <span class="slide__pill">週末限定</span>
            <h2 class="slide__title">焼きたてスコーンセット</h2>
            <p class="slide__sub">お好きなドリンクと自家製スコーンで¥780。</p>
          </div>
        </div>
      </div>
    </div>
    <button class="nav nav--prev" data-prev aria-label="前へ">‹</button>
    <button class="nav nav--next" data-next aria-label="次へ">›</button>
    <div class="dots" data-dots></div>
  </div>
</div>
CSS
/* MOON BREW カフェ テーマ */
:root{--cream:#f5ede1;--brown:#2b1d12;--amber:#c98a3b}
*{box-sizing:border-box}
body{
  margin:0;min-height:100vh;display:grid;place-items:center;padding:14px;
  font-family:"Hiragino Mincho ProN","Segoe UI",serif;
  background:var(--cream);color:var(--brown);
}
.cafe{width:min(560px,100%)}
.cafe__bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.cafe__logo{font-weight:700;letter-spacing:.06em}
.cafe__tag{font-size:.72rem;letter-spacing:.25em;color:var(--amber)}
.carousel{position:relative;border-radius:16px;overflow:hidden;box-shadow:0 10px 30px rgba(43,29,18,.18)}
.carousel__viewport{overflow:hidden}
.carousel__track{display:flex;transition:transform .5s cubic-bezier(.4,0,.2,1)}
.slide{
  position:relative;flex:0 0 100%;height:300px;
  background-size:cover;background-position:center;color:#fff;
}
.slide::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(43,29,18,.78),rgba(43,29,18,.05))}
.slide__copy{position:relative;z-index:1;max-width:60%;padding:34px 30px}
.slide__pill{
  display:inline-block;padding:4px 12px;border-radius:999px;
  background:var(--amber);color:#2b1d12;font-size:.7rem;font-weight:700;letter-spacing:.14em;
}
.slide__title{margin:12px 0 8px;font-size:1.6rem;letter-spacing:.04em}
.slide__sub{margin:0;font-size:.9rem;line-height:1.7;opacity:.92}
.nav{
  position:absolute;top:50%;transform:translateY(-50%);
  width:38px;height:38px;border:none;border-radius:50%;cursor:pointer;
  background:rgba(255,255,255,.85);color:#2b1d12;font-size:1.4rem;line-height:1;
  display:grid;place-items:center;transition:background .2s;
}
.nav:hover{background:#fff}
.nav--prev{left:12px}
.nav--next{right:12px}
.dots{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);display:flex;gap:8px}
.dot{width:8px;height:8px;border:none;border-radius:50%;background:rgba(255,255,255,.5);cursor:pointer;padding:0}
.dot.is-active{background:var(--amber);transform:scale(1.3)}
@media (prefers-reduced-motion:reduce){.carousel__track{transition:none}}
JavaScript
// 自動再生・矢印・ドット・スワイプ対応カルーセル
const root = document.querySelector('[data-carousel]');
if (root) {
  const track = root.querySelector('.carousel__track');
  const slides = [...root.querySelectorAll('.slide')];
  const dotsBox = root.querySelector('[data-dots]');
  let index = 0;
  let timer = null;
  const INTERVAL = 3800;

  // ドット生成
  slides.forEach((_, i) => {
    const dot = document.createElement('button');
    dot.className = 'dot';
    dot.setAttribute('aria-label', `${i + 1}枚目へ`);
    dot.addEventListener('click', () => { go(i); restart(); });
    dotsBox.appendChild(dot);
  });
  const dots = [...dotsBox.children];

  // 指定スライドへ移動
  const go = (i) => {
    index = (i + slides.length) % slides.length;
    track.style.transform = `translateX(-${index * 100}%)`;
    dots.forEach((d, di) => d.classList.toggle('is-active', di === index));
  };
  const next = () => go(index + 1);
  const prev = () => go(index - 1);

  root.querySelector('[data-next]')?.addEventListener('click', () => { next(); restart(); });
  root.querySelector('[data-prev]')?.addEventListener('click', () => { prev(); restart(); });

  // 自動再生(reduce-motion 時は止める)
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const start = () => { if (!reduce) timer = setInterval(next, INTERVAL); };
  const stop = () => { clearInterval(timer); timer = null; };
  const restart = () => { stop(); start(); };

  root.addEventListener('mouseenter', stop);
  root.addEventListener('mouseleave', start);

  // ポインタによるスワイプ
  let startX = null;
  const vp = root.querySelector('.carousel__viewport');
  vp.addEventListener('pointerdown', (e) => { startX = e.clientX; stop(); });
  vp.addEventListener('pointerup', (e) => {
    if (startX === null) return;
    const dx = e.clientX - startX;
    if (Math.abs(dx) > 40) (dx < 0 ? next : prev)();
    startX = null;
    start();
  });

  go(0);
  start();
}

コード

HTML
<!-- カルーセル:自動再生・矢印・ドット・スワイプ対応のスライダー -->
<div class="carousel" data-carousel>
  <div class="carousel__viewport">
    <ul class="carousel__track">
      <li class="slide" style="--from:#6366f1;--to:#ec4899">
        <span class="slide__num">01</span>
        <h3>新しい体験をデザインする</h3>
        <p>滑らかな移動とスワイプ操作に対応した軽量カルーセル。</p>
      </li>
      <li class="slide" style="--from:#0ea5e9;--to:#22d3ee">
        <span class="slide__num">02</span>
        <h3>自動再生とホバー停止</h3>
        <p>マウスを乗せると一時停止。離すと再び動き出します。</p>
      </li>
      <li class="slide" style="--from:#f59e0b;--to:#ef4444">
        <span class="slide__num">03</span>
        <h3>ドットでジャンプ</h3>
        <p>インジケーターをクリックして任意のスライドへ。</p>
      </li>
      <li class="slide" style="--from:#10b981;--to:#84cc16">
        <span class="slide__num">04</span>
        <h3>レスポンシブ対応</h3>
        <p>幅に応じて自動でフィット。外部ライブラリ不要。</p>
      </li>
    </ul>
  </div>

  <button class="carousel__arrow carousel__arrow--prev" data-prev aria-label="前へ">&#10094;</button>
  <button class="carousel__arrow carousel__arrow--next" data-next aria-label="次へ">&#10095;</button>
  <div class="carousel__dots" data-dots></div>
</div>
CSS
:root{
  --bg:#0b1220;
  --text:#fff;
  --radius:18px;
}
*{box-sizing:border-box}
body{
  margin:0;min-height:100vh;
  display:grid;place-items:center;padding:24px;
  font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
  background:radial-gradient(700px 360px at 50% -10%,#1a2540,transparent),var(--bg);
}
.carousel{
  position:relative;width:min(480px,100%);
  border-radius:var(--radius);
  box-shadow:0 30px 60px -28px rgba(0,0,0,.7);
}
.carousel__viewport{overflow:hidden;border-radius:var(--radius)}
/* トラックを translateX でスライド */
.carousel__track{
  display:flex;margin:0;padding:0;list-style:none;
  transition:transform .5s cubic-bezier(.4,0,.2,1);
}
.slide{
  flex:0 0 100%;min-height:240px;
  display:flex;flex-direction:column;justify-content:center;gap:8px;
  padding:34px 30px;
  background:linear-gradient(135deg,var(--from),var(--to));
}
.slide__num{
  font-size:2.4rem;font-weight:800;opacity:.35;line-height:1;
}
.slide h3{margin:6px 0 0;font-size:1.4rem}
.slide p{margin:0;max-width:30ch;line-height:1.6;opacity:.92;font-size:.92rem}
/* 矢印 */
.carousel__arrow{
  position:absolute;top:50%;transform:translateY(-50%);
  width:42px;height:42px;border:none;border-radius:50%;cursor:pointer;
  background:rgba(0,0,0,.35);color:#fff;font-size:1rem;
  display:grid;place-items:center;
  backdrop-filter:blur(4px);transition:background .2s,transform .15s;
}
.carousel__arrow:hover{background:rgba(0,0,0,.6)}
.carousel__arrow:active{transform:translateY(-50%) scale(.92)}
.carousel__arrow--prev{left:12px}
.carousel__arrow--next{right:12px}
/* ドット */
.carousel__dots{
  position:absolute;left:0;right:0;bottom:14px;
  display:flex;justify-content:center;gap:8px;
}
.dot{
  width:9px;height:9px;border-radius:50%;border:none;cursor:pointer;padding:0;
  background:rgba(255,255,255,.45);transition:width .3s,background .3s;
}
.dot.is-active{width:22px;border-radius:6px;background:#fff}
@media (prefers-reduced-motion:reduce){.carousel__track{transition:none}}
JavaScript
// 自動再生・矢印・ドット・スワイプ対応カルーセル
const root = document.querySelector('[data-carousel]');
if (root) {
  const track = root.querySelector('.carousel__track');
  const slides = [...root.querySelectorAll('.slide')];
  const dotsBox = root.querySelector('[data-dots]');
  let index = 0;
  let timer = null;
  const INTERVAL = 3500;

  // ドット生成
  slides.forEach((_, i) => {
    const dot = document.createElement('button');
    dot.className = 'dot';
    dot.setAttribute('aria-label', `${i + 1}枚目へ`);
    dot.addEventListener('click', () => go(i));
    dotsBox.appendChild(dot);
  });
  const dots = [...dotsBox.children];

  // 指定スライドへ移動
  const go = (i) => {
    index = (i + slides.length) % slides.length;
    track.style.transform = `translateX(-${index * 100}%)`;
    dots.forEach((d, di) => d.classList.toggle('is-active', di === index));
  };
  const next = () => go(index + 1);
  const prev = () => go(index - 1);

  root.querySelector('[data-next]')?.addEventListener('click', () => { next(); restart(); });
  root.querySelector('[data-prev]')?.addEventListener('click', () => { prev(); restart(); });

  // 自動再生(reduce-motion 時は止める)
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const start = () => { if (!reduce) timer = setInterval(next, INTERVAL); };
  const stop = () => { clearInterval(timer); timer = null; };
  const restart = () => { stop(); start(); };

  root.addEventListener('mouseenter', stop);
  root.addEventListener('mouseleave', start);

  // タッチ/ポインタによるスワイプ
  let startX = null;
  const vp = root.querySelector('.carousel__viewport');
  vp.addEventListener('pointerdown', (e) => { startX = e.clientX; stop(); });
  vp.addEventListener('pointerup', (e) => {
    if (startX === null) return;
    const dx = e.clientX - startX;
    if (Math.abs(dx) > 40) (dx < 0 ? next : prev)();
    startX = null;
    start();
  });

  go(0);
  start();
}

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「カルーセル / スライダー」の効果を追加してください。

# 追加してほしい効果
カルーセル / スライダー(UIコンポーネント)
自動再生・矢印・ドット・スワイプに対応した軽量カルーセル。translateXによる横移動で、バナーや特集の回遊表示に使えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- カルーセル:自動再生・矢印・ドット・スワイプ対応のスライダー -->
<div class="carousel" data-carousel>
  <div class="carousel__viewport">
    <ul class="carousel__track">
      <li class="slide" style="--from:#6366f1;--to:#ec4899">
        <span class="slide__num">01</span>
        <h3>新しい体験をデザインする</h3>
        <p>滑らかな移動とスワイプ操作に対応した軽量カルーセル。</p>
      </li>
      <li class="slide" style="--from:#0ea5e9;--to:#22d3ee">
        <span class="slide__num">02</span>
        <h3>自動再生とホバー停止</h3>
        <p>マウスを乗せると一時停止。離すと再び動き出します。</p>
      </li>
      <li class="slide" style="--from:#f59e0b;--to:#ef4444">
        <span class="slide__num">03</span>
        <h3>ドットでジャンプ</h3>
        <p>インジケーターをクリックして任意のスライドへ。</p>
      </li>
      <li class="slide" style="--from:#10b981;--to:#84cc16">
        <span class="slide__num">04</span>
        <h3>レスポンシブ対応</h3>
        <p>幅に応じて自動でフィット。外部ライブラリ不要。</p>
      </li>
    </ul>
  </div>

  <button class="carousel__arrow carousel__arrow--prev" data-prev aria-label="前へ">&#10094;</button>
  <button class="carousel__arrow carousel__arrow--next" data-next aria-label="次へ">&#10095;</button>
  <div class="carousel__dots" data-dots></div>
</div>

【CSS】
:root{
  --bg:#0b1220;
  --text:#fff;
  --radius:18px;
}
*{box-sizing:border-box}
body{
  margin:0;min-height:100vh;
  display:grid;place-items:center;padding:24px;
  font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
  background:radial-gradient(700px 360px at 50% -10%,#1a2540,transparent),var(--bg);
}
.carousel{
  position:relative;width:min(480px,100%);
  border-radius:var(--radius);
  box-shadow:0 30px 60px -28px rgba(0,0,0,.7);
}
.carousel__viewport{overflow:hidden;border-radius:var(--radius)}
/* トラックを translateX でスライド */
.carousel__track{
  display:flex;margin:0;padding:0;list-style:none;
  transition:transform .5s cubic-bezier(.4,0,.2,1);
}
.slide{
  flex:0 0 100%;min-height:240px;
  display:flex;flex-direction:column;justify-content:center;gap:8px;
  padding:34px 30px;
  background:linear-gradient(135deg,var(--from),var(--to));
}
.slide__num{
  font-size:2.4rem;font-weight:800;opacity:.35;line-height:1;
}
.slide h3{margin:6px 0 0;font-size:1.4rem}
.slide p{margin:0;max-width:30ch;line-height:1.6;opacity:.92;font-size:.92rem}
/* 矢印 */
.carousel__arrow{
  position:absolute;top:50%;transform:translateY(-50%);
  width:42px;height:42px;border:none;border-radius:50%;cursor:pointer;
  background:rgba(0,0,0,.35);color:#fff;font-size:1rem;
  display:grid;place-items:center;
  backdrop-filter:blur(4px);transition:background .2s,transform .15s;
}
.carousel__arrow:hover{background:rgba(0,0,0,.6)}
.carousel__arrow:active{transform:translateY(-50%) scale(.92)}
.carousel__arrow--prev{left:12px}
.carousel__arrow--next{right:12px}
/* ドット */
.carousel__dots{
  position:absolute;left:0;right:0;bottom:14px;
  display:flex;justify-content:center;gap:8px;
}
.dot{
  width:9px;height:9px;border-radius:50%;border:none;cursor:pointer;padding:0;
  background:rgba(255,255,255,.45);transition:width .3s,background .3s;
}
.dot.is-active{width:22px;border-radius:6px;background:#fff}
@media (prefers-reduced-motion:reduce){.carousel__track{transition:none}}

【JavaScript】
// 自動再生・矢印・ドット・スワイプ対応カルーセル
const root = document.querySelector('[data-carousel]');
if (root) {
  const track = root.querySelector('.carousel__track');
  const slides = [...root.querySelectorAll('.slide')];
  const dotsBox = root.querySelector('[data-dots]');
  let index = 0;
  let timer = null;
  const INTERVAL = 3500;

  // ドット生成
  slides.forEach((_, i) => {
    const dot = document.createElement('button');
    dot.className = 'dot';
    dot.setAttribute('aria-label', `${i + 1}枚目へ`);
    dot.addEventListener('click', () => go(i));
    dotsBox.appendChild(dot);
  });
  const dots = [...dotsBox.children];

  // 指定スライドへ移動
  const go = (i) => {
    index = (i + slides.length) % slides.length;
    track.style.transform = `translateX(-${index * 100}%)`;
    dots.forEach((d, di) => d.classList.toggle('is-active', di === index));
  };
  const next = () => go(index + 1);
  const prev = () => go(index - 1);

  root.querySelector('[data-next]')?.addEventListener('click', () => { next(); restart(); });
  root.querySelector('[data-prev]')?.addEventListener('click', () => { prev(); restart(); });

  // 自動再生(reduce-motion 時は止める)
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const start = () => { if (!reduce) timer = setInterval(next, INTERVAL); };
  const stop = () => { clearInterval(timer); timer = null; };
  const restart = () => { stop(); start(); };

  root.addEventListener('mouseenter', stop);
  root.addEventListener('mouseleave', start);

  // タッチ/ポインタによるスワイプ
  let startX = null;
  const vp = root.querySelector('.carousel__viewport');
  vp.addEventListener('pointerdown', (e) => { startX = e.clientX; stop(); });
  vp.addEventListener('pointerup', (e) => {
    if (startX === null) return;
    const dx = e.clientX - startX;
    if (Math.abs(dx) > 40) (dx < 0 ? next : prev)();
    startX = null;
    start();
  });

  go(0);
  start();
}

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。