コピペで使えるJS未使用のシンプルなCSSカルーセルスライダー

限られたスペースに複数の情報を載せたい場合、非常に重宝するのがスライドショーとも呼ばれているカルーセルスライダーです。

JavaScriptで実装するのが一般的で「bxSlider」「slick」「Swiper」といったJSライブラリ (JSプラグイン) がよく利用されています。

しかし、これらのJSライブラリは多機能な反面、ファイルサイズが大きく、ウェブサイト内のごく限られた箇所にだけ使用したいといった場合には、利用しづらいのではないでしょうか?

そこで、今回はコピペできるCSSカルーセルスライダーを紹介します。

今回作る各種スライダー

See the Pen CSS Slider by nov (@numerofive) on CodePen.

カルーセルでないスライダー

まずは、とてもシンプルな、スクロールバーでスライドさせるスライダーを作ってみます。

HTML
<div class="slide">
  <ul class="slide_contents">
    <li class="slide_content">スライドさせる内容1</li>
    <li class="slide_content">スライドさせる内容2</li>
    <li class="slide_content">スライドさせる内容3</li>
    <li class="slide_content">スライドさせる内容4</li>
  </ul>
</div>
CSS
.slide_contents {
  display: flex;
  list-style: none;
  margin: 0;
    padding: 0;
  overflow-x: auto;
  scroll-behavior: smooth;
  scroll-snap-type: x mandatory;
}
.slide_content {
  flex: none;
  scroll-snap-align: center;
  width: 100%;
}

scroll-snap-typeプロパティ、scroll-snap-alignプロパティを使用することで、内容部分にピタっと止まるようになります。場合によっては、これで十分なケースもあるかもしれません。

カルーセルスライダー

カルーセルでないスライダーとHTMLコードこそ同じですが、CSSコードはかなり複雑になります。

CSS
/*
内容が右から左に流れるアニメーションを設定します。
*/
@keyframes SLIDE_4 {
  0% {
    transform: translateX(100%);
    visibility: visible;
  }

  /*
  4つの内容を順番に表示させるので、全体の長さを 100% としたとき、
  1つの内容を表示する長さは 100% / 4 で 25% になります。
  0% から 25% / 10 の 2.5% を徐々に表れるアニメーションにあてます。
  */
  2.5%, 25% {
    transform: translateX(0%);
  }

  /*
  25% + 2.5% の 27.5% 以降は非表示に戻します。
  */
  27.5% {
    visibility: hidden;
  }

  /*
  25% から 27.5% を徐々に消えるアニメーションにあてます。
  27.5% 以降はアニメーションさせません。
  */
  27.5%, 100% {
    transform: translateX(-100%);
  }
}

.slide_contents {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;

  /*
  はみ出た部分を隠します。
  */
  overflow: hidden;
}
.slide_content {
  flex: none;
  width: 100%;

  /*
  アニメーション (SLIDE_4) を適用します。
  1つの内容を表示させる長さを 4s (4秒) としたとき、
  全体の長さは 16s (16秒) になります。
  */
  animation: SLIDE_4 16s infinite;

  /*
  すべての内容を重ねます。
  */
  margin-inline-end: -100%;

  /*
  アニメーションの開始前は非表示にします。
  */
  visibility: hidden;
}
.slide_content:nth-child(1) {
  /*
  徐々に表れる 0.4s (4s / 10) を省きます。
  */
  animation-delay: -0.4s;
}
.slide_content:nth-child(2) {
  /*
  1つ目から 4s (4s - 0.4s) 遅らせます。
  */
  animation-delay: 3.6s;
}
.slide_content:nth-child(3) {
  /*
  1つ目から 8s (8s - 0.4s) 遅らせます。
  */
  animation-delay: 7.6s;
}
.slide_content:nth-child(4) {
  /*
  1つ目から 12s (12s - 0.4s) 遅らせます。
  */
  animation-delay: 11.6s;
}

計算が必要になる箇所が多くあります。上記のコードは4つの内容を4秒ずつ表示させた場合です。

内容の数が増減したり、表示させる秒数を変更する場合には、その都度、計算する必要が出てくるので不便です。そこで、CSS変数 (CSSカスタムプロパティ) のvar関数とcalc関数を活用します。

var関数とcalc関数を使用したCSS
/*
アニメーションを設定します。
CSS変数が設定されていなければ初期値を設定します。
*/
@keyframes SLIDE_2 {
  0%        { transform: var(--transform-1, none); visibility: var(--visibility-1, visible); }
  5%, 50%   { transform: var(--transform-2, none); visibility: }
  55%       { var(--visibility-2, visible); }
  55%, 100% { transform: var(--transform-3, none); visibility: }
}
@keyframes SLIDE_3 {
  0%                    { transform: var(--transform-1, none); visibility: var(--visibility-1, visible); }
  3.333333%, 33.333333% { transform: var(--transform-2, none); }
  36.666666%            { visibility: var(--visibility-2, visible); }
  36.666666%, 100%      { transform: var(--transform-3, none); }
}
@keyframes SLIDE_4 {
  0%          { transform: var(--transform-1, none); visibility: var(--visibility-1, visible); }
  2.5%, 25%   { transform: var(--transform-2, none); }
  27.5%       { visibility: var(--visibility-2, visible); }
  27.5%, 100% { transform: var(--transform-3, none); }
}

.slide {
  /*
  ベースとなる時間のCSS変数を設定します。
  */
  --time: 4s;
}
.slide_contents {
  /*
  内容が右から左に流れるアニメーションのCSS変数を設定します。
  */
  --transform-1: translateX(100%);
  --transform-2: translateX(0%);
  --transform-3: translateX(-100%);
  --visibility-2: hidden;

  display: flex;
  list-style: none;
  margin: 0;
    padding: 0;
  overflow: hidden;
}
.slide_content {
  flex: none;
  margin-inline-end: -100%;
  visibility: hidden;
  width: 100%;
}

/*
アニメーションを適用します。
*/
.slide_content:nth-child(1):nth-last-child(2),
:nth-child(1):nth-last-child(2) ~ .slide_content {
  animation: SLIDE_2 calc(var(--time) * 2) infinite;
}
.slide_content:nth-child(1):nth-last-child(3),
:nth-child(1):nth-last-child(3) ~ .slide_content {
  animation: SLIDE_3 calc(var(--time) * 3) infinite;
}
.slide_content:nth-child(1):nth-last-child(4),
:nth-child(1):nth-last-child(4) ~ .slide_content {
  animation: SLIDE_4 calc(var(--time) * 4) infinite;
}
.slide_content:nth-child(1):nth-last-child(n) {
  animation-delay: calc(var(--time) / -10);
}
.slide_content:nth-child(2):nth-last-child(n) {
  animation-delay: calc(var(--time) / -10 + var(--time));
}
.slide_content:nth-child(3):nth-last-child(n) {
  animation-delay: calc(var(--time) / -10 + var(--time) * 2);
}
.slide_content:nth-child(4):nth-last-child(n) {
  animation-delay: calc(var(--time) / -10 + var(--time) * 3);
}

このように設定することで、HTML側からの設定の変更も可能になり、アニメーションのバリエーションも簡単に増やすことができるようになります。

CSS変数の値を変更するHTML
<!--
1つの内容が表示される長さを 6s に変更。
-->
<div class="slide" style="--time: 6s;">
  <!--
  内容が左から右に流れるアニメーションに変更。
  -->
  <ul class="slide_contents" style="--transform-1: translateX(-100%); --transform-2: translateX(0%); --transform-3: translateX(100%);">
    <li class="slide_content">スライドさせる内容1</li>
    <li class="slide_content">スライドさせる内容2</li>
    <li class="slide_content">スライドさせる内容3</li>
    <li class="slide_content">スライドさせる内容4</li>
  </ul>
</div>

アニメーションのバリエーションをクラス毎にまとめれば、CSS側で一括管理もできます。

アニメーションをクラス毎にまとめるCSS
/* 右から左へ (Right To Left) */
.slide_contents.is-animation-translate-rtl {
  --transform-1: translateX(100%);
  --transform-2: translateX(0%);
  --transform-3: translateX(-100%);
}

/* 左から右へ (Left To Right) */
.slide_contents.is-animation-translate-ltr {
  --transform-1: translateX(-100%);
  --transform-2: translateX(0%);
  --transform-3: translateX(100%);
}

/* 下から上へ (Bottom To Top) */
.slide_contents.is-animation-translate-btt {
  --transform-1: translateY(100%);
  --transform-2: translateY(0%);
  --transform-3: translateY(-100%);
}

/* 上から下へ (Top To Bottom) */
.slide_contents.is-animation-translate-ttb {
  --transform-1: translateY(-100%);
  --transform-2: translateY(0%);
  --transform-3: translateY(100%);
}

ちなみに、li.slide_content にCSS変数で番号を振ると、CSSコードの一部が簡略化できます。

CSSコードを簡略化するためのHTML
<div class="slide" style="--time: 6s;">
  <ul class="slide_contents is-animation-translate-btt">
    <li class="slide_content" style="--num: 1;">スライドさせる内容1</li>
    <li class="slide_content" style="--num: 2;">スライドさせる内容2</li>
    <li class="slide_content" style="--num: 3;">スライドさせる内容3</li>
    <li class="slide_content" style="--num: 4;">スライドさせる内容4</li>
  </ul>
</div>
簡略化されたCSSコード
.slide_content:nth-child(1) {
  animation-delay: calc(var(--time) / -10);
}
.slide_content:nth-child(2) {
  animation-delay: calc(var(--time) / -10 + var(--time));
}
.slide_content:nth-child(3) {
  animation-delay: calc(var(--time) / -10 + var(--time) * 2);
}
.slide_content:nth-child(4) {
  animation-delay: calc(var(--time) / -10 + var(--time) * 3);
}
.slide_content {
  animation-delay: calc(var(--time) / -10 + var(--time) * (var(--num) - 1));
}

コントロール付きカルーセルスライダー

ここまでで、無限に回り続けるカルーセルスライダーは作れましたが、内容をじっくり見たい (見せたい) 場合もありますよね。なので、アニメーションを無効化して、選択された内容が表示されるようにしてみます。

コントロール付きカルーセルスライダーのHTML
<div class="slide" style="--time: 6s;">

  <!--
  スライドをコントロールするためのラジオボタン
  -->
  <input type="radio" name="slide_radio" id="slide_radio_1" class="slide_radio select" />
  <input type="radio" name="slide_radio" id="slide_radio_2" class="slide_radio select" />
  <input type="radio" name="slide_radio" id="slide_radio_3" class="slide_radio select" />
  <input type="radio" name="slide_radio" id="slide_radio_4" class="slide_radio select" />
  <input type="radio" name="slide_radio" id="slide_radio_0" class="slide_radio unselect" checked="checked" />

  <ul class="slide_contents is-animation-translate-btt">
    <li class="slide_content">スライドさせる内容1</li>
    <li class="slide_content">スライドさせる内容2</li>
    <li class="slide_content">スライドさせる内容3</li>
    <li class="slide_content">スライドさせる内容4</li>
  </ul>

  <!--
  ラジオボタンを操作するためのラベル
  -->
  <div class="slide_labels">
    <label for="slide_radio_1" class="slide_label select"></label>
    <label for="slide_radio_2" class="slide_label select"></label>
    <label for="slide_radio_3" class="slide_label select"></label>
    <label for="slide_radio_4" class="slide_label select"></label>
    <label for="slide_radio_0" class="slide_label unselect"></label>
  </div>

</div>

イメージとしては、input.slide_radio.unselect が checked のときにアニメーションが適用され、input.slide_radio.select が checked のとき、それに対応した内容が表示されるという感じになります。

コントロール付きカルーセルスライダーのCSS
/*
アニメーションを設定します。
label.slide_label.select にもアニメーションを適用するため、
opacityプロパティを追加しています。
*/
@keyframes SLIDE_2 {
  0%        { opacity: var(--opacity-1, 1); transform: var(--transform-1, none); visibility: var(--visibility-1, visible); }
  5%, 50%   { opacity: var(--opacity-2, 1); transform: var(--transform-2, none); }
  55%       { visibility: var(--visibility-2, visible); }
  55%, 100% { opacity: var(--opacity-3, 1); transform: var(--transform-3, none); }
}
@keyframes SLIDE_3 {
  0%                    { opacity: var(--opacity-1, 1); transform: var(--transform-1, none); visibility: var(--visibility-1, visible); }
  3.333333%, 33.333333% { opacity: var(--opacity-2, 1); transform: var(--transform-2, none); }
  36.666666%            { visibility: var(--visibility-2, visible); }
  36.666666%, 100%      { opacity: var(--opacity-3, 1); transform: var(--transform-3, none); }
}
@keyframes SLIDE_4 {
  0%          { opacity: var(--opacity-1, 1); transform: var(--transform-1, none); visibility: var(--visibility-1, visible); }
  2.5%, 25%   { opacity: var(--opacity-2, 1); transform: var(--transform-2, none); }
  27.5%       { visibility: var(--visibility-3, visible); }
  27.5%, 100% { opacity: var(--opacity-3, 1); transform: var(--transform-3, none); }
}

.slide {
  --time: 4s;
}
.slide_contents {
  --visibility-2: hidden;

  display: flex;
  list-style: none;
  margin: 0;
    padding: 0;
  overflow: hidden;
}
.slide_contents:not([class^="is-animation-"]):not([class*=" is-animation-"]):not([style*="--"]),
.slide_contents.is-animation-translate-rtl {
  --transform-1: translateX(100%);
  --transform-2: translateX(0%);
  --transform-3: translateX(-100%);
}
.slide_contents.is-animation-translate-ltr {
  --transform-1: translateX(-100%);
  --transform-2: translateX(0%);
  --transform-3: translateX(100%);
}
.slide_contents.is-animation-translate-btt {
  --transform-1: translateY(100%);
  --transform-2: translateY(0%);
  --transform-3: translateY(-100%);
}
.slide_contents.is-animation-translate-ttb {
  --transform-1: translateY(-100%);
  --transform-2: translateY(0%);
  --transform-3: translateY(100%);
}
.slide_contents.is-animation-fade {
  --opacity-1: 0;
  --opacity-3: 0;
}
.slide_contents.is-animation-zoom-in {
  --opacity-1: 0;
  --opacity-3: 0;
  --transform-1: scale(0.5);
  --transform-2: scale(1);
  --transform-3: scale(2);
}
.slide_contents.is-animation-zoom-out {
  --opacity-1: 0;
  --opacity-3: 0;
  --transform-1: scale(2);
  --transform-2: scale(1);
  --transform-3: scale(0.5);
}
.slide_contents.is-animation-rotate-x {
  --opacity-1: 0;
  --opacity-3: 0;
  --transform-1: rotateX(-180deg);
  --transform-2: rotateX(0deg);
  --transform-3: rotateX(180deg);
}
.slide_contents.is-animation-rotate-y {
  --opacity-1: 0;
  --opacity-3: 0;
  --transform-1: rotateY(-180deg);
  --transform-2: rotateY(0deg);
  --transform-3: rotateY(180deg);
}
.slide_contents.is-animation-rotate-z {
  --opacity-1: 0;
  --opacity-3: 0;
  --transform-1: rotate(-180deg);
  --transform-2: rotate(0deg);
  --transform-3: rotate(180deg);
}
.slide_contents.is-animation-skew-x {
  --opacity-1: 0;
  --opacity-3: 0;
  --transform-1: skewX(180deg);
  --transform-2: skewX(0deg);
  --transform-3: skewX(-180deg);
}
.slide_contents.is-animation-skew-y {
  --opacity-1: 0;
  --opacity-3: 0;
  --transform-1: skewY(-180deg);
  --transform-2: skewY(0deg);
  --transform-3: skewY(180deg);
}
.slide_content {
  flex: none;
  margin-inline-end: -100%;
  visibility: hidden;
  width: 100%;
}

/*
ラベルのスタイルを設定します。
label.slide_label.select と label.slide_label.unselect が、
トグルボタンのようにクリックできるようにします。
+ Image ------+------------------------------+------------------------------+
| z-index: 2; | .select:unselected:clickable |                              |
| z-index: 1; | .unselect:unclickable        | .unselect:clickable          |
| z-index: 0; |                              | .select:selected:unclickable |
+-------------+------------------------------+------------------------------+
*/
.slide_labels {
  /*
  アニメーションのCSS変数を設定します。
  ラベルに対応した内容が表示されているときは opacity: 1;
  そうでないときは opacity: 0.25; が適用されます。
  */
  --opacity-1: 0.25;
  --opacity-3: 0.25;

  display: flex;
  margin: 0 auto;
  position: relative;
  width: min-content;
}
.slide_label.select {
  aspect-ratio: 1 / 1;
  cursor: pointer;
  display: grid;
    place-items: center;
  opacity: 0.25;
  width: 40px;
  z-index: 2;
}
.slide_label.select:before {
  aspect-ratio: 1 / 1;
  background: currentcolor;
  border-radius: 50%;
  content: "";
  display: block;
  transition: transform 0.2s;
  width: 20%;
}
.slide_label.select:hover:before {
  transform: scale(1.5);
}
.slide_label.unselect {
  cursor: pointer;
  position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
  z-index: 1;
}

/*
アニメーションを内容とラベルに適用します。
*/
.slide_content:nth-child(1):nth-last-child(2),
:nth-child(1):nth-last-child(2) ~ .slide_content,
.slide_label.select:nth-child(1):nth-last-child(3),
:nth-child(1):nth-last-child(3) ~ .slide_label.select {
  animation: SLIDE_2 calc(var(--time) * 2) infinite;
}
.slide_content:nth-child(1):nth-last-child(3),
:nth-child(1):nth-last-child(3) ~ .slide_content,
.slide_label.select:nth-child(1):nth-last-child(4),
:nth-child(1):nth-last-child(4) ~ .slide_label.select {
  animation: SLIDE_3 calc(var(--time) * 3) infinite;
}
.slide_content:nth-child(1):nth-last-child(4),
:nth-child(1):nth-last-child(4) ~ .slide_content,
.slide_label.select:nth-child(1):nth-last-child(5),
:nth-child(1):nth-last-child(5) ~ .slide_label.select {
  animation: SLIDE_4 calc(var(--time) * 4) infinite;
}
.slide_content:nth-child(1):nth-last-child(n),
.slide_label.select:nth-child(1):nth-last-child(n) {
  animation-delay: calc(var(--time) / -10);
}
.slide_content:nth-child(2):nth-last-child(n),
.slide_label.select:nth-child(2):nth-last-child(n) {
  animation-delay: calc(var(--time) / -10 + var(--time));
}
.slide_content:nth-child(3):nth-last-child(n),
.slide_label.select:nth-child(3):nth-last-child(n) {
  animation-delay: calc(var(--time) / -10 + var(--time) * 2);
}
.slide_content:nth-child(4):nth-last-child(n),
.slide_label.select:nth-child(4):nth-last-child(n) {
  animation-delay: calc(var(--time) / -10 + var(--time) * 3);
}

/*
ラジオボタンと、ラジオボタンが checked のときの内容とラベルのスタイルを設定します。
*/
.slide_radio {
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  position: absolute;
  visibility: hidden;
}
.slide_radio.select:checked ~ .slide_contents > .slide_content,
.slide_radio.select:checked ~ .slide_labels > .slide_label.select {
  animation: none;
}
.slide_radio.select:nth-child(1):checked ~ .slide_contents > .slide_content:nth-child(1),
.slide_radio.select:nth-child(2):checked ~ .slide_contents > .slide_content:nth-child(2),
.slide_radio.select:nth-child(3):checked ~ .slide_contents > .slide_content:nth-child(3),
.slide_radio.select:nth-child(4):checked ~ .slide_contents > .slide_content:nth-child(4) {
  visibility: visible;
}
.slide_radio.select:nth-child(1):checked ~ .slide_labels > .slide_label.select:nth-child(1),
.slide_radio.select:nth-child(2):checked ~ .slide_labels > .slide_label.select:nth-child(2),
.slide_radio.select:nth-child(3):checked ~ .slide_labels > .slide_label.select:nth-child(3),
.slide_radio.select:nth-child(4):checked ~ .slide_labels > .slide_label.select:nth-child(4) {
  opacity: 1;
  z-index: 0;
}

これで、よく見るカルーセルスライダーの完成です。

使用する上での注意点としては、1つのページで複数のカルーセルスライダーを設置する場合、ラジオボタンに設定するname属性とid属性がページ上で重複しないようにする必要があります。

また、WordPressのクラシックエディタで使用する場合、自動でp要素を追加する機能により、正しく動作しない場合があります。

WordPressのクラシックエディタにコピペした際のHTML
<div class="slide"><input type="radio" … /> … <input type="radio" … /></p>
  <ul class="slide_contents is-animation-translate-btt">
    …
  </ul>
  <div class="slide_labels">
    …
  </div>
</div>

こういった場合は、the_contentフィルターフックで、余計なタグを除去する処理を追加すると良いです。

WordPressの投稿本文から余計なタグを除去するPHPコードの一例
add_filter( 'the_content', 'the_content__remove_tags', 12 );
if( !function_exists( 'the_content__remove_tags' ) ):
function the_content__remove_tags( $content ) {
  // `</p></p>` => `</p>`
  // `<div><inlineElement></p>` => `<div><inlineElement>`
  if( '' !== $content ) {
    // 投稿本文を `</p>` で分割して配列にする。
    $strings = explode( '</p>', $content );
    $content = '';
    foreach( $strings as $string ) {
      $content .= $string;
      // 配列の値に `<p>` か`<p ` がある場合にのみ `</p>` を追加する。
      if( false !== strpos( $string, '<p>' ) || false !== strpos( $string, '<p ' ) ) {
        $content .= '</p>';
      }
    }#endforeach
  }
  return $content;
}
endif;

JavaScriptが書ける人は、ラジオボタンなんか使わずに、JavaScriptで実装した方が楽です。でも、それ以外の部分はCSSでもいけますね。

関連記事