GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

css 画像スライダー9選 — コピペで使えるカルーセル

通販サイトの商品写真や会社サイトのトップ画像で、指でなぞったりボタンを押したりすると次の一枚に切り替わる仕組みを画像スライダーと呼びます。ここで紹介するcss 画像スライダーはCSSだけ・JS不要で、9種類のやり方をそのままコピペできる形にまとめました。

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

9個は「css スライドショー 作り方」の人気順ではなく、カルーセルが画面で担う役割の順に並べています。指で動かす一番基本の形(01)から、点・矢印・サムネイルで操作方法を示すもの(02〜04)に進み、そのあとは何も押さなくても自分から回るもの(05〜06)、最後は今どれを見ているかを強調する飾り的なもの(07〜09)という並びです。何枚目を見ているのかをバーで知らせたいならスクロール進捗表示がその目盛りを作り、最初の画面をカルーセルで始めるつもりならヒーローセクションのアニメーションが前後を揃えてくれます。09の3Dカバーフローまで来ると、カルーセルは画像の帯ではなく場面そのものを運ぶようになります。

01基本scroll-snap

指やホイールで止まる位置

横に並んだ4枚の写真カードを指やマウスホイールで動かすと、画面にぴったり収まる位置でカードが止まります。overflow-x: autoとscroll-snap-typeのふたつのプロパティだけで動くので、JavaScriptは1行も要りません。

基本scroll-snap
新しいタブで開く

01 · 基本scroll-snap — 計測 累積面積 62.372% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scroll-snap-typescroll-snap-alignJS 0줄

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でスライドごとに自動で作ります。

::scroll-markerドット
新しいタブで開く

02 · ::scroll-markerドット — 計測 累積面積 49.997% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

::scroll-markerscroll-marker-group:target-current

点はブラウザが作る

.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枚ずつ

「css カルーセル ボタン 付き」で探す形で、スライドの両脇にある前後の矢印ボタンを押すと、写真が1枚ずつなめらかに切り替わります。矢印ボタンもブラウザが::scroll-button()で自動的に生成し、端まで来ると薄くなって押せなくなります。

::scroll-button矢印
新しいタブで開く

03 · ::scroll-button矢印 — 計測 累積面積 56.113% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

::scroll-button(*):disabledscroll-snap-type

端まで来ると押せなくなる

.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をそのまま受け継いでいます。

サムネイルマーカー
新しいタブで開く

04 · サムネイルマーカー — 計測 累積面積 49.928% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

::scroll-markerCSS 변수 상속:target-current

CSS変数を受け継ぐ

.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だけで自動再生

「css カルーセル 自動」を止めるボタンなしで

何も押さなくても、写真が数秒ごとに自分から次のコマへ切り替わります。トラックひとつを@keyframesで無限に回しているだけで、止めるボタンはなくマウスを乗せている間だけ一時停止します。

CSSだけで自動再生
新しいタブで開く

05 · CSSだけで自動再生 — 計測 累積面積 62.373% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@keyframesanimation-play-stateJS 0줄

マウスを乗せている間だけ止まる

.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枚ずつフェードで巡回する

お客さまの声のカードが1枚ずつフェードで切り替わりながら、自分から順番に巡回します。3枚とも同じgridのマスに重ねてあるので、文章の長さが違っても高さが揺れません。

レビュースライダー
新しいタブで開く

06 · レビュースライダー — 計測 累積面積 3.304% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

opacitygrid 겹침@keyframes

高さが揺れない理由

.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アクティブスライド拡大

中央だけ拡大、両隣は薄く

中央にある写真だけ少し大きく拡大され、両隣の写真は縮んで薄くなるので、今見ているものが一目でわかります。ラベルをクリックして別の写真を選ぶと、その写真が中央に来て再び大きくなります。

アクティブスライド拡大
新しいタブで開く

07 · アクティブスライド拡大 — 計測 累積面積 46.955% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

:checkedtransform: scaleopacity

クリックで中央に来る

.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進捗マーカー連動

切り替わるたびに埋まるバー

スライドの上に細い進捗バーがあり、写真が切り替わるたびにその分だけ埋まります。バーの上に透明なクリック領域を重ねてあるので、見たい番号のところを直接押しても切り替えられます。

進捗マーカー連動
新しいタブで開く

08 · 進捗マーカー連動 — 計測 累積面積 51.043% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

:checkedwidth 전환형제결합자

透明なクリック領域

.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カバーフロー

アルバムカバーをめくるように

中央の写真は正面に大きく立ち、両隣の写真は斜めに傾いて小さく退いているので、アルバムカバーを手でめくっているように見えます。好きな写真をクリックすると、その写真が正面中央に来ます。

3Dカバーフロー
新しいタブで開く

09 · 3Dカバーフロー — 計測 累積面積 36.764% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

perspectiverotateY + translateZ:checked

クリックした写真が正面へ

.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);
}

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

スクロール枠にtransformを掛けると

九つのうち一番深く踏んだ落とし穴は、01を作っているときにスクロール枠(overflow-x: autoが付いた要素)自体にtranslateXを掛けたことでした。translateX(-100%)の100%は常にその要素自身の幅を基準にするため、枠の中に入っている4枚ぶんの横幅ではなく、枠自身の幅(カード1枚ぶん、400px)しか動かず、カード全体がまるごと画面の外に消えてしまったのです。getBoundingClientRectで実測すると、動いたトラックはビューポート幅400pxをはるかに超えて-822pxまで移動していました。この修正で01〜04と08は、実際にスクロールする外枠(viewport)と、そのなかでtransformだけを受け持つ帯(track)を分けた三層構造に組み直しています。

:disabledは境界で消えきらない

枠の計算がずれて写真が切れて見える状況は、CSS画像スライドアニメーションでも同じアスペクト比の規則で解けます。

もうひとつ、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

「css だけ で カルーセル」の::scroll-markerはどのブラウザでも使えますか?

いいえ。執筆時点では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で渡します。矢印やドットまで備えた完成形のスライダーはCSSカルーセルスライダーの作り方で続けて組み立てます。

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

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