GODRICH

css カルーセル スライダーの作り方9選 — コピペでOK

css カルーセル スライダーとは、写真やカードを何枚か並べて画面の中で順番に切り替えて見せる部品のことです。指でめくったりボタンを押したりすると次の一枚が現れ、JavaScriptを書かなくてもCSSだけで動かせます。スクロールからラジオ技まで、9種類の作り方をそのままコピペして使える形でまとめました。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

9個は人気順ではなく、実際にカルーセルを組むときに手を付ける順に並べています。指でめくる最も基本の形(01)から、矢印・ドット・サムネイルで操作方法を変えていく段階(02〜04)へ進み、そのあとスクロールする向きと見え方を変える段階(05〜06)を経て、最後は何も押さなくても自分から動いたり今どの辺りにいるかを教えてくれる段階(07〜09)という流れです。

01次のスライドが少し見える

カード同士の間隔を広めに取ってあるので、今見えている一枚の隣にもう一枚がわずかにのぞきます。scroll-padding-leftで最初のカードは画面の左端にぴったり合わせて止まり、実際に指でドラッグしたりホイールで送ったりするだけなのでJavaScriptは要りません。通販サイトの商品カード一覧のように「まだ続きがある」と伝えたい場面に向いています。

scroll-paddingscroll-snap-alignJS 0줄
.pn__viewport {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: $sp-4;
}
.pn__track { display: flex; width: max-content; }
.pn__slide {
  flex: 0 0 200px;
  aspect-ratio: 4 / 3;
  scroll-snap-align: start;
  margin-right: $sp-3;
}

02ラジオ+矢印スライダー

隠したラジオボタン3個と矢印形のラベルだけでスライドを1枚ずつ送る、スクロール不要のもっとも古典的なCSSカルーセルの作り方です。矢印を押すと対応するラジオが選ばれ、トラック全体が33.33%ずつ横へ動きます。バナーや、手動でだけ送りたいお知らせカードに向いています。

:checked형제결합자label for
.ra__track {
  display: flex;
  width: 300%;
  transition: transform $duration $easing;
}
#ra-1:checked ~ .ra__viewport .ra__track { transform: translateX(0%); }
#ra-2:checked ~ .ra__viewport .ra__track { transform: translateX(-33.3333%); }
#ra-3:checked ~ .ra__viewport .ra__track { transform: translateX(-66.6667%); }

03:targetドットナビ

ドットを押すとアドレスバーの#が本物のリンクのように変わり、ブラウザがそのスライドをスナップトラックの中で画面に合わせて動かします。:targetセレクターは今指しているスライドを判定するためだけに使い、:has()で該当するドットだけを塗ります。戻るボタンで前のスライドに戻したいギャラリーや、URLだけで特定の一枚を共有したい場面に向いています。

:targetscroll-margin앵커 링크
.dt__viewport { overflow-x: auto; scroll-snap-type: x mandatory; }
.dt__slide { scroll-snap-align: start; }
.dt:has(#dt-s1:target) .dt__dot--1,
.dt:has(#dt-s2:target) .dt__dot--2,
.dt:has(#dt-s3:target) .dt__dot--3 {
  background: $color;
  transform: scale(1.4);
}

04サムネイル連動スナップ

大きな写真の下に本物のサムネイル画像の列が別にあり、それぞれが対応するスライドへのアンカーになっています。:targetで今見ている写真を判定し、同じ番号のサムネイルにだけ枠線が付きます。商品詳細ページの画像ギャラリーや、複数カットの写真をめくる場面に向いています。

:target형제결합자scroll-snap-align
.tm__thumb {
  border: 3px solid transparent;
  transition: border-color $duration $easing;
}
.tm:has(#tm-s1:target) .tm__thumb--1,
.tm:has(#tm-s2:target) .tm__thumb--2,
.tm:has(#tm-s3:target) .tm__thumb--3 {
  border-color: $subject-ink;
}

05縦スナップ

横ではなく縦にスクロールし、画面の高さいっぱいにスライドが1枚ずつぴったり止まります。overflow-yscroll-snap-type: yを使うだけで、スマホのストーリー画面と同じ向きのカルーセルになります。縦型のストーリーバナーやモバイルのフルスクリーン紹介スライドに向いています。

scroll-snap-type: yscroll-snap-align100%
.vs__viewport {
  height: 100%;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
}
.vs__track { display: flex; flex-direction: column; height: max-content; }
.vs__slide { flex: 0 0 150px; width: 100%; scroll-snap-align: start; }

06カバーフロースナップ

ラジオボタンは一切使わず、実際の横スクロールとscroll-snap-align: centerだけで動きます。中央に来たカードは正面を向き、両隣のカードは最初から斜めに傾いた状態で自分の位置に置かれています。ポートフォリオの作品めくりや、アルバム・グッズのカバーを見せる場面に向いています。

perspectivescroll-snap-align: centerrotateY
.cf__viewport {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  perspective: 640px;
}
.cf__slide { scroll-snap-align: center; }
.cf__slide--1, .cf__slide--5 { transform: rotateY(38deg) scale(.72); }
.cf__slide--2, .cf__slide--4 { transform: rotateY(24deg) scale(.86); }
.cf__slide--3 { transform: rotateY(0deg) scale(1); }

07カードスタックスワイプ

カードを何枚も同じ位置に重ねて置き、一番上のカードが横に滑って抜けると次のカードがその場所を埋めます。スクロールする箱そのものがなく、同じ@keyframesをカードごとにanimation-delayだけずらして自動で循環させています。マッチングアプリのようなカードめくりや、おすすめカードを1枚ずつ見せる場面に向いています。

grid-areatransformz-index
.cs__card {
  grid-area: 1 / 1;
  animation: cs-cycle $dur-loop $easing infinite;
}
.cs__card--1 { animation-delay: -0s; }
.cs__card--2 { animation-delay: -.667s; }
.cs__card--3 { animation-delay: -1.333s; }
@keyframes cs-cycle {
  0%, 4%   { transform: translate(0, 0); opacity: 1; z-index: 3; }
  29%      { transform: translate(42%, 6%) rotate(12deg); opacity: 0; }
  33%, 62% { transform: translate(-6%, 6%) rotate(-4deg); opacity: .85; }
  66%, 96% { transform: translate(6%, 4%) rotate(3deg); opacity: .92; }
  100%     { transform: translate(0, 0); opacity: 1; z-index: 3; }
}

08ケンバーンズ自動再生ヒーロー

写真が自動で切り替わる間、今画面に映っている一枚だけがゆっくり拡大し続けるので、静止画なのにずっと動いているように見えます。クロスフェードで写真を切り替えるアニメーションに、表示されている間だけscale(1)からscale(1.16)へ拡大するアニメーションを重ねています。メインのヒーローバナーやブランド紹介のフルスクリーンスライドに向いています。

transform: scale@keyframesopacity
.kb__card { grid-area: 1 / 1; animation: kb-fade-1 $dur-loop linear infinite; }
.kb__img  { animation: kb-zoom-1 $dur-loop $easing infinite; }
@keyframes kb-fade-1 { 0%, 4%  { opacity: 1; } 29%, 100% { opacity: 0; } }
@keyframes kb-zoom-1 {
  0%   { transform: scale(1); }
  29%  { transform: scale(1.16); }
  29.1%, 100% { transform: scale(1); }
}

09スクロール連動プログレスバー

ラジオボタンもJavaScriptの計算も使わず、animation-timelineが実際の横スクロール位置をそのまま読み取って進捗バーの幅を埋めます。バーとスクロール枠は兄弟要素なので、共通の親に指定したtimeline-scopeがその名前を橋渡ししています。スライドが何枚あるか一目で示したいストーリーバナーに向いています。

animation-timeline: scroll()scroll-timelineJS 0줄
.sp { timeline-scope: --sp-progress; }
.sp__viewport { scroll-timeline: --sp-progress inline; }
.sp__fill {
  animation-name: sp-fill;
  animation-timeline: --sp-progress;
  animation-fill-mode: both;
}
@keyframes sp-fill {
  from { width: 0%; }
  to   { width: 100%; }
}

どこで壊れるか — 落とし穴ひとつ

09番(スクロール連動プログレスバー)を最初に作ったとき、進捗バーがまったく動きませんでした。進捗バーの.sp__fillとスクロール枠の.sp__viewportは兄弟要素で、scroll-timelineで名付けたタイムラインは既定ではその名前を宣言した要素の子孫からしか見えないため、兄弟側のバーからは参照できなかったのです。共通の祖先である.sptimeline-scope: --sp-progressを書き足して初めて、その名前が兄弟側にも開かれ、実際のスクロール位置どおりにバーが伸びていくのを確認できました。インストール済みのChromeで実際にレンダリングし、00→12→22フレームを見比べると、バーが0%→約50%→100%と本当に埋まっていく様子まで実測できています。9個のフォルダーをこうして確かめ直してから圧縮したzipの合言葉はb2earrkfで、この段落の中にそのまま書いてあるのでここからコピーして使ってください。

アクセシビリティ

OSでモーションを減らす設定にしていると、07・08は自動で循環する動きが止まり、最初のカードだけが固定して表示されます。01・03・04・05・06・09はプレビュー用のループアニメーションだけが止まり、実際のスクロールとscroll-snapそのものは生きたまま残ります。03・04のドットとサムネイルにはaria-label="スライド1"のような番号付きラベルを付けており、02のラジオスライダーはネイティブのフォームコントロールなのでTabキーと矢印キーだけで選べます。詳しい対応状況はMDNのprefers-reduced-motionドキュメントで確認できます。

スクロールを使わない別のカルーセルの作り方はCSSカルーセルまとめにもあり、CSSだけで動く他のアニメーションはCSSカテゴリーから探せます。

FAQ

ラジオスライダーとスクロールスナップ、どちらを使えばいいですか?

実際に指でドラッグする感触が欲しいならスクロールスナップ(01・03〜06・09)を、矢印やドットだけできっちり切り替えたいならラジオ方式(02)を使います。スクロールスナップはタッチ端末でそのまま指で送れるのが強みで、ラジオ方式はトラックの位置をラジオひとつで正確に固定できるのが強みです。

animation-timelineはすべてのブラウザで動きますか?

インストール済みのChromeで実際にレンダリングして値を測った結果なので、Chromium系の外までは保証できません。ただし止まるのは09番の進捗バーだけで、スクロールスナップ自体はそのまま動くため、スライドを送る操作には支障がありません。

React版でも仕組みは同じですか?

01・05・06・07・08・09はCSSモジュールに同じスクロールスナップ・@keyframesanimation-timelineをそのまま移すので原理は同じです。02(ラジオ)はuseStateひとつに置き換え、03・04(:target)は同じページに複数置いてもIDが衝突しないようscrollIntoViewに切り替えています。durationeasingcolorはどれもpropsで受け取ります。

scroll-snap-typeの正確な仕様はMDNのscroll-snap-typeドキュメントで、animation-timelineMDNのanimation-timelineドキュメントで確認できます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。