css 画像スライダー9選 — コピペで使えるカルーセル
通販サイトの商品写真や会社サイトのトップ画像で、指でなぞったりボタンを押したりすると次の一枚に切り替わる仕組みを画像スライダーと呼びます。ここで紹介するcss 画像スライダーはCSSだけ・JS不要で、9種類のやり方をそのままコピペできる形にまとめました。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 基本scroll-snap
- 02 ::scroll-markerドット
- 03 ::scroll-button矢印
- 04 サムネイルマーカー
- 05 CSSだけで自動再生
- 06 レビュースライダー
- 07 アクティブスライド拡大
- 08 進捗マーカー連動
- 09 3Dカバーフロー
9個は人気順ではなく、カルーセルが画面で担う役割の順に並べています。指で動かす一番基本の形(01)から、点・矢印・サムネイルで操作方法を示すもの(02〜04)に進み、そのあとは何も押さなくても自分から回るもの(05〜06)、最後は今どれを見ているかを強調する飾り的なもの(07〜09)という並びです。
01基本scroll-snap
横に並んだ4枚の写真カードを指やマウスホイールで動かすと、画面にぴったり収まる位置でカードが止まります。overflow-x: autoとscroll-snap-typeのふたつのプロパティだけで動くので、JavaScriptは1行も要りません。
.cs__viewport {
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.cs__track { display: flex; width: 400%; }
.cs__slide {
flex: 0 0 25%;
scroll-snap-align: start;
}
02::scroll-markerドット
スライドの下に小さな点が並び、今見ている写真の点だけ色が付いて少し大きくなります。点はJavaScriptで描いたものではなく、ブラウザが::scroll-markerでスライドごとに自動で作ります。
.dm__viewport { scroll-marker-group: after; }
.dm__slide::scroll-marker {
content: "";
width: 8px; height: 8px;
border-radius: 50%;
}
.dm__slide::scroll-marker:target-current {
background: $color;
transform: scale(1.35);
}
03::scroll-button矢印
スライドの両脇にある前後の矢印ボタンを押すと、写真が1枚ずつなめらかに切り替わります。矢印ボタンもブラウザが::scroll-button()で自動的に生成し、端まで来ると薄くなって押せなくなります。
.ab__viewport::scroll-button(left) { content: "‹"; }
.ab__viewport::scroll-button(right) { content: "›"; }
.ab__viewport::scroll-button(left):disabled,
.ab__viewport::scroll-button(right):disabled {
opacity: 0;
pointer-events: none;
}
04サムネイルマーカー
大きな写真の下に正方形の小さなサムネイルが並び、今見ている写真と同じ色のサムネイルにだけ枠が付きます。サムネイルも::scroll-markerひとつで作っていて、色は元のスライドに書いたCSS変数--thumbをそのまま受け継いでいます。
.tm__slide {
background: linear-gradient(135deg, var(--thumb), color-mix(in srgb, var(--thumb) 55%, white));
}
.tm__slide::scroll-marker {
content: "";
background: var(--thumb);
border: 2px solid transparent;
}
.tm__slide::scroll-marker:target-current {
border-color: $color;
}
05CSSだけで自動再生
何も押さなくても、写真が数秒ごとに自分から次のコマへ切り替わります。トラックひとつを@keyframesで無限に回しているだけで、止めるボタンはなくマウスを乗せている間だけ一時停止します。
.ap__track {
display: flex;
width: 400%;
animation: ap-cycle 2s ease-out infinite;
}
.ap:hover .ap__track {
animation-play-state: paused;
}
@keyframes ap-cycle {
0%, 17% { transform: translateX(0%); }
25%, 42% { transform: translateX(-25%); }
50%, 67% { transform: translateX(-50%); }
75%, 92% { transform: translateX(-75%); }
100% { transform: translateX(0%); }
}
06レビュースライダー
お客さまの声のカードが1枚ずつフェードで切り替わりながら、自分から順番に巡回します。3枚とも同じgridのマスに重ねてあるので、文章の長さが違っても高さが揺れません。
.ts__stack { display: grid; }
.ts__card {
grid-area: 1 / 1;
opacity: 0;
}
.ts__card--1 { animation: ts-1 2s ease-out infinite; }
.ts__card--2 { animation: ts-2 2s ease-out infinite; }
.ts__card--3 { animation: ts-3 2s ease-out infinite; }
@keyframes ts-1 { 0%, 4% { opacity: 1; } 29%, 100% { opacity: 0; } }
07アクティブスライド拡大
中央にある写真だけ少し大きく拡大され、両隣の写真は縮んで薄くなるので、今見ているものが一目でわかります。ラベルをクリックして別の写真を選ぶと、その写真が中央に来て再び大きくなります。
.as__slide {
transform: scale(.82);
opacity: .5;
transition: transform .3s, opacity .3s;
}
#as-2:checked ~ .as__track .as__slide--2 {
transform: scale(1.08);
opacity: 1;
}
08進捗マーカー連動
スライドの上に細い進捗バーがあり、写真が切り替わるたびにその分だけ埋まります。バーの上に透明なクリック領域を重ねてあるので、見たい番号のところを直接押しても切り替えられます。
.pm__fill {
width: calc(100% / 3);
transition: width .3s;
}
#pm-2:checked ~ .pm__bar .pm__fill { width: calc(200% / 3); }
#pm-3:checked ~ .pm__bar .pm__fill { width: 100%; }
.pm__track {
transform: translateX(0%);
transition: transform .3s;
}
#pm-2:checked ~ .pm__viewport .pm__track { transform: translateX(calc(-100% / 3)); }
093Dカバーフロー
中央の写真は正面に大きく立ち、両隣の写真は斜めに傾いて小さく退いているので、アルバムカバーを手でめくっているように見えます。好きな写真をクリックすると、その写真が正面中央に来ます。
.cf__stage { perspective: 720px; }
.cf__slide { transform-style: preserve-3d; }
#cf-2:checked ~ .cf__stage .cf__slide--2 {
transform: translateX(-50%) rotateY(0deg) scale(1);
}
#cf-2:checked ~ .cf__stage .cf__slide--3 {
transform: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(.74);
}
どこで壊れるか — 落とし穴
九つのうち一番深く踏んだ落とし穴は、01を作っているときにスクロール枠(overflow-x: autoが付いた要素)自体にtranslateXを掛けたことでした。translateX(-100%)の100%は常にその要素自身の幅を基準にするため、枠の中に入っている4枚ぶんの横幅ではなく、枠自身の幅(カード1枚ぶん、400px)しか動かず、カード全体がまるごと画面の外に消えてしまったのです。getBoundingClientRectで実測すると、動いたトラックはビューポート幅400pxをはるかに超えて-822pxまで移動していました。この修正で01〜04と08は、実際にスクロールする外枠(viewport)と、そのなかでtransformだけを受け持つ帯(track)を分けた三層構造に組み直しています。
もうひとつ、03の::scroll-button()にある:disabled状態は、Chromiumベースのブラウザで試しても境界できれいに消えきらないコマが実測で見つかりました。矢印がうっすら残って見えるコマがあっても、実際にクリックが効かなくなるかどうかはこの見た目の変化とは別に、スクロールが端で止まるブラウザ標準の動きに任せています。この9jk4bv6cは、外枠と帯を分け終えた九つのファイルを開きます。
アクセシビリティ
prefers-reduced-motion: reduceでは九つとも自動再生やデモ用のループアニメーションが止まり、今表示している状態のまま固定されます。01〜04はブラウザが管理するスクロール枠そのものなので、フォーカスが当たれば矢印キーだけでスライドを送れます。07・08・09はラジオボタンで作ったネイティブのフォームコントロールなので、Tabキーと矢印キーで選べ、07・09の各ラベルにはaria-label="スライド1"のような番号付きの説明を付けています。06は<figure>・<blockquote>・<figcaption>で意味のあるマークアップにしてあり、05は止めるボタンを持たずマウスを乗せている間だけ一時停止する仕組みなので、マウスのない環境では止め方が限られる点は知っておく価値があります。
ポートフォリオやランディングページに使えるほかのCSSアニメーションはCSSアニメーションの一覧にまとめてあり、サイトを作っている人については紹介ページに書いています。scroll-snap-typeの詳しい仕様はMDNのscroll-snap-typeのページで確認でき、::scroll-marker・::scroll-button()のような新しい機能の対応状況はcaniuseで随時変わります。
FAQ
::scroll-marker・::scroll-buttonはどのブラウザでも使えますか?
いいえ。執筆時点ではChromiumベースのブラウザだけで動作を直接確認していて、実装前にCSS.supports()で対応の有無を調べてから使っています。対応していないブラウザでは点や矢印だけが表示されず、01の指でなぞるscroll-snap-typeそのものは変わらず動くので、基本の操作方法は失われません。
自動再生中にユーザーがスライドを操作したらどうなりますか?
01〜04・07〜09はスライドに最初に触れた瞬間(pointerdown)にデモ用のループクラスが外れ、そのあとは実際のスクロールやクリックだけが反映されます。デモのループと本物の状態が同時に競合することはありません。
Reactで作るときも仕組みは同じですか?
01〜04はscroll-snap-typeや::scroll-marker・::scroll-button()をそのままCSSモジュールに移せば同じ原理で動きます。07〜09のようにもとはラジオボタンで選んでいた項目だけuseStateひとつに置き換え、duration・easing・colorはpropsで渡します。