テクニック

Shopifyスライダーセクションの実装|ライブラリなしの作り方

この記事の対象: Shopifyテーマのコード編集に触れたことがある制作者
読了時間: 約12分

Shopifyのストアにスライダーを入れたいとき、Swiper.jsやSlickなどのライブラリが候補に上がる。だが、CSSの scroll-snap と少しのJavaScriptを組み合わせれば、外部ファイルの読み込みなしでスライダーが作れる。追加リクエストがゼロなぶん表示速度に有利で、ライブラリのバージョン管理からも解放される。

この記事では、管理画面から画像を追加・並べ替えできるスライダーセクションを1ファイルで作る。自動再生、矢印ボタン、インジケーター(ドット)、スマホのスワイプ、画像の遅延読み込みまで含めた完成形のコードを掲載する。


スライダーセクション実装の前提

スライダーセクション実装の前提

動作環境

  • Online Store 2.0対応テーマ(Dawn、Craft、Sense など。2021年以降に公開された公式テーマはすべて対応)
  • テーマコードを編集できるスタッフ権限
  • scroll-snap の対応ブラウザ:Chrome 69以降、Firefox 68以降、Safari 11以降、Edge 79以降

Online Store 1.0(Debutなど旧テーマ)ではセクションの仕組みが異なるため、そのままでは動かない。テーマ選びの段階から2.0を前提にしておくのが無難だ。

ファイルの配置場所

作成するのは1ファイルだけ。テーマエディタの「コードを編集」画面で sections フォルダに新規ファイルを追加する。

sections/custom-slider.liquid

このファイルにHTML(Liquid)・CSS・JavaScript・schemaをすべてまとめる。以降のコードブロックは、すべてこのファイルの中身を上から順に示している。

セクションファイルの構成(上から順に記述)
Stylescroll-snapとレイアウトのCSS
HTML / Liquid画像の出力とUIパーツ
Script操作と自動再生の処理
Schema管理画面の設定項目

4つのパートが1ファイルに収まる。Shopifyのセクションファイルはこの構成が標準で、アセットを分ける必要はない。


セクションファイルのコード全体

セクションファイルのコード全体

CSS:scroll-snapでスライドを制御する

<style>
  .slider-{{ section.id }} {
    position: relative;
    overflow: hidden;
  }

  .slider__track-{{ section.id }} {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .slider__track-{{ section.id }}::-webkit-scrollbar {
    display: none;
  }

  .slider__track-{{ section.id }} > * {
    flex: 0 0 100%;
    scroll-snap-align: start;
  }

  .slider-{{ section.id }} img {
    display: block;
    width: 100%;
    height: {{ section.settings.height }}px;
    object-fit: cover;
  }

  .slider-{{ section.id }} .slider__arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    background: rgba(255, 255, 255, 0.85);
    border: none;
    border-radius: 50%;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    z-index: 2;
  }

  .slider-{{ section.id }} .slider__arrow:hover {
    background: #fff;
  }

  .slider-{{ section.id }} .slider__prev { left: 12px; }
  .slider-{{ section.id }} .slider__next { right: 12px; }

  .slider-{{ section.id }} .slider__dots {
    display: flex;
    justify-content: center;
    gap: 8px;
    padding: 12px 0;
  }

  .slider-{{ section.id }} .slider__dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 2px solid #333;
    background: transparent;
    padding: 0;
    cursor: pointer;
    transition: background 0.2s;
  }

  .slider-{{ section.id }} .slider__dot.is-active {
    background: #333;
  }

  @media (max-width: 749px) {
    .slider-{{ section.id }} .slider__arrow {
      width: 36px;
      height: 36px;
      font-size: 16px;
    }
  }
</style>

ポイントは3つ。

  • scroll-snap-type: x mandatory をトラックに、scroll-snap-align: start を各スライドに指定する。これだけでスクロール位置がスライド単位にスナップし、スマホのスワイプ操作にも対応する
  • scrollbar-width: none と ::-webkit-scrollbar { display: none } でスクロールバーを消す。消さないとデスクトップで横スクロールバーが出る
  • セレクタに {{ section.id }} を含めることで、同じページに複数のスライダーを置いてもCSSが衝突しない

height は schema の設定値を使っている。管理画面のスライダーで200px〜800pxの範囲を選べるようにする。

HTML / Liquid:画像とUIパーツを出力する

<div
  class="slider-{{ section.id }}"
  data-autoplay="{{ section.settings.autoplay }}"
  data-interval="{{ section.settings.interval }}"
>
  <div class="slider__track-{{ section.id }}">
    {%- for block in section.blocks -%}
      <div {{ block.shopify_attributes }}>
        {%- if block.settings.link != blank -%}
          <a href="{{ block.settings.link }}">
        {%- endif -%}

        {%- if block.settings.image != blank -%}
          {%- if forloop.first -%}
            {{ block.settings.image | image_url: width: 1500 | image_tag:
              loading: 'eager',
              widths: '375, 750, 1100, 1500',
              sizes: '100vw'
            }}
          {%- else -%}
            {{ block.settings.image | image_url: width: 1500 | image_tag:
              loading: 'lazy',
              widths: '375, 750, 1100, 1500',
              sizes: '100vw'
            }}
          {%- endif -%}
        {%- else -%}
          {{ 'image' | placeholder_svg_tag }}
        {%- endif -%}

        {%- if block.settings.link != blank -%}
          </a>
        {%- endif -%}
      </div>
    {%- endfor -%}
  </div>

  {%- if section.blocks.size > 1 -%}
    <button class="slider__arrow slider__prev" aria-label="前のスライド">&#10094;</button>
    <button class="slider__arrow slider__next" aria-label="次のスライド">&#10095;</button>
    <div class="slider__dots">
      {%- for block in section.blocks -%}
        <button
          class="slider__dot{% if forloop.first %} is-active{% endif %}"
          aria-label="スライド{{ forloop.index }}"
        ></button>
      {%- endfor -%}
    </div>
  {%- endif -%}
</div>

画像の読み込み順を分けているのがこのコードの肝。forloop.first(最初の1枚)だけ loading: 'eager' で即座に読み込み、2枚目以降は loading: 'lazy' にしている。ファーストビューに表示されるスライドだけを先に取得し、見えていない画像のリクエストを後回しにする仕組みだ。

image_tag フィルターの widths に複数サイズを渡すと、ブラウザが画面幅に合った画像を自動で選ぶ srcset が出力される。1500px幅の端末に375pxの画像を送らずに済む。

矢印とドットは、スライドが2枚以上あるときだけ表示する。1枚しかないときにUIが出ても邪魔になるだけなので section.blocks.size > 1 で分岐している。

JavaScript:矢印・ドット・自動再生の処理

<script>
  (function () {
    const root = document.querySelector('.slider-{{ section.id }}');
    if (!root) return;

    const track = root.querySelector('.slider__track-{{ section.id }}');
    const slides = [...track.children];
    const dots = root.querySelectorAll('.slider__dot');
    const prev = root.querySelector('.slider__prev');
    const next = root.querySelector('.slider__next');
    const autoplay = root.dataset.autoplay === 'true';
    const ms = parseInt(root.dataset.interval, 10) || 5000;
    let current = 0;
    let timer = null;

    function go(i) {
      if (i < 0) i = slides.length - 1;
      if (i >= slides.length) i = 0;
      current = i;
      track.scrollTo({ left: i * track.offsetWidth, behavior: 'smooth' });
      dots.forEach((d, n) => d.classList.toggle('is-active', n === i));
    }

    prev?.addEventListener('click', () => { halt(); go(current - 1); });
    next?.addEventListener('click', () => { halt(); go(current + 1); });
    dots.forEach((d, i) => d.addEventListener('click', () => { halt(); go(i); }));

    let scrollTimer;
    track.addEventListener('scroll', () => {
      clearTimeout(scrollTimer);
      scrollTimer = setTimeout(() => {
        const i = Math.round(track.scrollLeft / track.offsetWidth);
        if (i !== current) {
          current = i;
          dots.forEach((d, n) => d.classList.toggle('is-active', n === i));
        }
      }, 80);
    }, { passive: true });

    function start() {
      if (autoplay) timer = setInterval(() => go(current + 1), ms);
    }
    function halt() { clearInterval(timer); }

    root.addEventListener('mouseenter', halt);
    root.addEventListener('mouseleave', start);
    root.addEventListener('touchstart', halt, { passive: true });
    root.addEventListener('touchend', () => {
      if (autoplay) setTimeout(start, 3000);
    }, { passive: true });

    start();
  })();
</script>

track.scrollTo() でスクロール位置をスライド幅の倍数に飛ばす。scroll-snap がかかっているので、端数が出てもスライドの先頭にぴったり止まる。

scroll イベントのリスナーは、スワイプ操作でスクロール位置が変わったときにドットの is-active を同期させるためのもの。80ミリ秒のデバウンスで、スクロール中に何十回も処理が走るのを防いでいる。

自動再生は mouseenter(マウスホバー)と touchstart(タッチ操作)で一時停止し、離したら再開する。touchend では3秒の猶予を置いている。操作直後にすぐ切り替わると、ユーザーがリンクをタップしにくくなるためだ。

schema:管理画面の設定項目

{% schema %}
{
  "name": "スライダー",
  "tag": "section",
  "settings": [
    {
      "type": "range",
      "id": "height",
      "label": "スライドの高さ",
      "min": 200,
      "max": 800,
      "step": 50,
      "default": 450,
      "unit": "px"
    },
    {
      "type": "checkbox",
      "id": "autoplay",
      "label": "自動再生",
      "default": true
    },
    {
      "type": "range",
      "id": "interval",
      "label": "切り替え間隔",
      "min": 2000,
      "max": 10000,
      "step": 500,
      "default": 5000,
      "unit": "ms"
    }
  ],
  "blocks": [
    {
      "type": "slide",
      "name": "スライド",
      "settings": [
        {
          "type": "image_picker",
          "id": "image",
          "label": "画像"
        },
        {
          "type": "url",
          "id": "link",
          "label": "リンク先URL"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "スライダー",
      "blocks": [
        { "type": "slide" },
        { "type": "slide" },
        { "type": "slide" }
      ]
    }
  ]
}
{% endschema %}

presets を入れておくと、テーマエディタの「セクションを追加」一覧に「スライダー」が表示される。選ぶだけで3枚ぶんの空スライドが初期状態として入る。

読者が変更すべき箇所:

項目 場所 初期値 変更の例
スライダーのファイル名 sections/ フォルダ custom-slider.liquid ストア名やセクション名に合わせて変更可
矢印の色 CSSの background: rgba(255,255,255,0.85) 白の半透明 背景が白い画像が多いなら暗い色に
ドットの色 CSSの border と background #333(黒) ブランドカラーに差し替え
高さの範囲 schemaの height 200〜800px トップページのメインビジュアル用途なら上限を広げてもよい
自動再生間隔 schemaの interval 5000ms テキスト量が多いスライドなら長めに設定

管理画面からスライダーを追加する手順

管理画面からスライダーを追加する手順

ファイルを保存したら、テーマエディタで操作する。

  1. オンラインストア > テーマ > カスタマイズ を開く
  2. スライダーを置きたいページ(トップページなど)を選ぶ
  3. 「セクションを追加」→「スライダー」を選ぶ
  4. 「スライド」ブロックの「画像」から画像をアップロード or メディアライブラリから選択
  5. 必要ならリンク先URLを入れる
  6. スライドの順番はドラッグで入れ替え可能

スライドは管理画面から何枚でも追加できる。ただし、表示速度を考えると5〜8枚が実用的な上限だ。それ以上並べるなら、ギャラリーページとして別途作るほうがユーザーにとっても扱いやすい。

スライダーに載せる画像素材の調達については、ライセンスの確認を含めてこちらの記事にまとめている。


スライダーのカスタマイズ

スライダーのカスタマイズ

高さを画像に合わせたい場合

schema で固定高を設定するのではなく、画像のアスペクト比をそのまま活かしたい場合は、CSSの height 指定を外す。

/* height: {{ section.settings.height }}px; を削除して以下に変更 */
.slider-{{ section.id }} img {
  display: block;
  width: 100%;
  height: auto;
}

ただし、スライドごとに画像の縦横比が違うとスライダーの高さが切り替わりのたびに変わり、がたつく。すべてのスライド画像を同じアスペクト比にトリミングしてからアップロードするか、object-fit: cover で固定高にするかの二択になる。

自動再生を使わないほうがよいケース

ECの商品ページやLPで、クリックさせたいボタンがスライド内にあるなら、自動再生は切ったほうがよい。操作中にスライドが動くとボタンを押しづらくなり、離脱につながる。管理画面の「自動再生」チェックを外すだけで停止できる。

Shopifyのカスタマイズで「ここまではできるが、ここからは無理」という境界線は事前に把握しておくと手戻りが減る。セクション開発でできる範囲と、アプリやAPIが必要になる範囲の違いは下記が参考になる。

ライブラリ有無による違い

CSS scroll-snap(今回の方法)

  • 追加の読み込みゼロ
  • スワイプはブラウザが処理
  • テーマ更新と干渉しない

外部ライブラリ

  • JS+CSSで数十KB追加
  • バージョン固定が必要
  • 設定オプションは豊富

スライドの枚数が10枚以下で、フェードやパララックスといった特殊エフェクトが不要なら、scroll-snapで十分に間に合う。


うまくいかないとき

うまくいかないとき

テーマエディタの「セクションを追加」に出てこない

{% schema %} の presets が抜けているか、JSON構文にエラーがある。ファイルを保存したときにShopifyがエラーを返していないか確認する。カンマの過不足やダブルクォートの閉じ忘れが多い。

スワイプできるがスナップしない

scroll-snap-type: x mandatory がトラックに、scroll-snap-align: start が各スライドに入っているか確認する。overflow-x: auto がトラック以外の親要素で hidden に上書きされていると、スナップが効かなくなる。

矢印を押しても動かない

JavaScriptのエラーをブラウザの開発者ツール(Console)で確認する。{{ section.id }} がLiquidで展開されるのはサーバーサイドなので、ブラウザのソースで .slider__track- のあとにIDが入っているか見る。空になっている場合、ファイルの拡張子が .liquid でない可能性がある。

画像が縦に潰れる・横に伸びる

アップロード画像の縦横比と、CSSの height 設定が合っていない。object-fit: cover が効いていれば比率は崩れないが、画像の上下または左右がトリミングされる。重要な被写体が端にある画像は、あらかじめ中央寄せにトリミングしておく。

ページ表示が遅い

2枚目以降の loading: 'lazy' が 'eager' になっていないか確認する。高解像度の元画像をそのまま使っている場合は、image_url フィルターの width を下げるか、widths パラメータを絞って不要な大きいサイズを生成しないようにする。画像まわりのパフォーマンス施策はShopifyのSEO対策|URL構造の制約とその回避策【2026年】でも触れている。


よくある質問

よくある質問

Shopifyのスライダーに外部ライブラリは必要ですか?

フェードやパララックスといった特殊なアニメーションが必要な場合を除き、不要です。CSSの scroll-snap でスライド切り替え・スワイプ・スナップがブラウザの標準機能として動きます。矢印やインジケーターも数十行のJavaScriptで実装できるため、追加読み込みなしで完結します。

scroll-snapはどのブラウザで動きますか?

Chrome 69以降、Firefox 68以降、Safari 11以降、Edge 79以降で対応しています。iOS SafariとAndroid Chromeもカバーしているので、2026年時点でサポート外のブラウザを気にする必要はほぼありません。

スライドは何枚まで入れられますか?

技術的な上限はありません。管理画面から何枚でもブロックを追加できます。ただし、枚数が増えるほどDOMの要素数と画像リクエストが増えるため、表示速度に影響が出ます。実用上は5〜8枚を目安にしてください。

動画をスライドに入れることはできますか?

この記事のコードは画像専用です。動画を入れるには、image_picker の代わりに video や video_url タイプの設定をschemaに追加し、Liquidで <video> タグを出力する改修が必要になります。自動再生との競合やモバイルでの再生制限も考慮する必要があるため、別途対応が要ります。

Online Store 1.0のテーマでも使えますか?

このコードはOnline Store 2.0のセクション構造(blocks を含む {% schema %})に依存しているため、1.0テーマ(Debutなど)ではそのまま使えません。1.0テーマの場合はセクションの仕組みが異なり、テーマを2.0対応のものに切り替えるか、テンプレートファイルに直接コードを埋め込む改修が必要になります。


まとめ

CSSの scroll-snap を軸にすれば、Shopifyのスライダーセクションは1ファイルで完結する。外部ライブラリのCDNリンクもnpmも不要で、管理画面から画像を差し替えられる仕組みがそのまま手に入る。

実装の手順をおさらいすると、sections/custom-slider.liquid を作り、CSS(scroll-snapでスナップ制御)・HTML/Liquid(画像出力と遅延読み込み)・JavaScript(矢印・ドット・自動再生)・schema(管理画面の設定UI)の4パートを書く。ファイルを保存すればテーマエディタの「セクションを追加」から使える。

表示速度を気にするストアほど、読み込むファイルは少ないほうがいい。まずはこのコードをそのまま入れて動かし、必要に応じて色や高さを調整してほしい。