完全レスポンシブなjQueryスライダーを自作|flexbox活用でresizeイベント不要・超軽量!

スライダー(カルーセル)って、プラグインとか使えば簡単に実装できるしオプションもあって便利なのですが、自作したくなることって一度はありますよね。

実際、凝った動きをしなくてもいいのであれば、自作しちゃったほうがコードも軽いし、予期せぬ誤作動をすることもありません。

javascript(jQuery)を使った画像スライダーの作り方はネットで幾らでも見つかるので、この記事ではちょっと変わったアプローチで、より短いコードで、ぬるぬるっとリサイズするスライダーのコードを紹介します。

よくある自作スライダーの不満な点

jQueryでクルクル回るスライダーといえば、以下のような処理がセオリーでしょう。

  1. ビューポートの幅とアイテム(画像)の数から、アイテムの塊のwidth(幅)を算出する。
  2. position: absoluteとoverflow:hiddenを使って1つのアイテムだけ見えるようにする。
  3. animate関数でアイテムの塊をスクロールさせる。
  4. 最後尾(最前列)のアイテムを前(後ろ)に移動させる。

slick等のスライダープラグインでも処理の流れは同じです。

スライダー自体の幅が固定なら要らないのですが、普通はビューポートの幅に合わせるため、①の処理が発生します。

ビューポートの幅に合わせてレスポンシブにリサイズしようとすると、resizeイベントで①の処理を繰り返さなければいけません。

これが格好悪いし、重いんですよね……

というわけで、スタイリングはCSSに任せて、widthの計算、ついでに要素移動も不要なサクサクレスポンシブスライダーを制作してみました。

軽いのでとってもスムーズに動いてくれます! スマホのフリック操作にも対応しています。

flexboxを使ったスライダーのデモ

さっそく実物です。

幅を縮めたりして、ぬるっと動くことをご確認ください。

See the Pen flex slider by Takayuki Kojima (@takayuki-kojima) on CodePen.

動き自体は、よくある普通のスライダーですね。

それではコードを解説していきます。

html

htmlは超シンプル、スタイリング用に<div>を何層にも重ねたりする必要はありません。

スライダーの枠になる<div>の中に、画像を入れたアイテムのリスト、操作用のボタンを入れるだけ。

アイテムは最低3個が必要で、3つ以上であれば何個でもjavascriptでの処理は同じです。

<div class=slider>
  <ul class=items>
    <li class=item><img src="../mt-fuji.jpg"></li>
    <li class=item><img src="../kilimanjaro.jpg"></li>
    <li class=item><img src="../aconcagua.jpg"></li>
    <li class=item><img src="../matterhorn.jpg"></li>
  </ul>
  <div class=buttons>
    <button type=button id=prev class="button prev"></button>
    <button type=button id=next class="button next"></button>
  </div>
</div>

WordPress用にもオススメ

ごく単純な構造なので、Wordpressに実装する場合も、何の変哲もないウィジェットでできちゃうので超らくちんです。

ただその場合は、アイテムが1個の時にボタンを消して動作を止める処理、アイテムが2個の時に複製して4個に増やす処理を、javascriptの冒頭に入れたほうが良いかもしれません。

CSS (SCSS)

CSSもわりとシンプル。

アイテムを並べるために、floatではなく、display: flexを使っているのが最大の特徴であり、動作のポイントです。

ボタンのスタイリングは省いているので、見たい方はデモのソースをご覧ください。簡単に言うと、display: flex; justify-content: space-between; align-items: center で左右の中央に配置してあります。

.slider{
  position: relative;
  width: 100%;
  padding-bottom: 56.25%;
  overflow: hidden;
}
.items{
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  position: absolute;
  z-index: 1;
  top: 0;
  left: -100%;
  width: 100%;
  height: 100%;
}
.item{
  min-width: 100%;
  height: 100%;
  img{
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

上記のコードをポイントごとに説明します。

スライダーの高さを決める

スライダー(.slider)の中身は全てposition: absoluteで配置するので、高さを指定する必要があります。

widthに対する%値で高さを取りたいので、heightではなくpaddingを使います。

16:9なら56.25%、4:3なら75%、といった容量ですね。

display: flexで並べる

ここがレスポンシブの肝です。

flexボックスの中身のアイテム(.item)にmin-width: 100%を指定すると、アイテムが縮小されるのを防ぐことができます。

しかもflex-wrap: nowrapだから、折り返すこともできない。

結果、全てのアイテムが100%の幅で横に並んでくれます!

position: absoluteで初期位置をセット

アイテムの塊(.items)をposition: absoluteとleft: -100%でアイテム1個分左にずらして配置します。

以上、jQueryを使うことなく、左から2番目のアイテムが見えている状態を実現できました。

object-fit: coverでトリミング

これはオマケ。

以上でアイテムのサイズは決まっているので、中に画像、テキスト、ビデオ、何を入れても構いません。

今回は画像が綺麗に収まるようにobject-fit: coverを使っています。

jQuery

いよいよ動かしていきます。処理の流れは以下の通り。

  1. animate関数でアイテムの塊(.items)を左右に100%動かす
  2. orderプロパティを利用してアイテム(.item)の並び順を操作する

①はよくあるパターンと一緒ですが、②で通常append/prepend等を使って要素を移動させるところ、このスライダーはflexボックスのorderプロパティを使って見かけの順番だけを変えているのです。

コード全体はデモのソースを見ていただくとして、少しずつ解説します。

初期値の設定

まずは処理の準備として以下の処理をします。

  1. 処理全体を通して使う変数や配列を定義して、値を入れる
  2. 各アイテムのorderプロパティに値を振る
var items = $('.items'), //アイテムの塊を指すオブジェクト
    item = [],            //各アイテムをオブジェクトとして格納する配列
    order = [],           //各アイテムのorderプロパティに入る値を格納する配列
    num = items.children().length, //アイテムの数
    index = 0,            //配列を操作するために使う変数
    running = false;      //アニメーションの重複作動を防ぐためのフラグ

for(var i = 0; i < num; i++){            //アイテムの数だけ繰り返し
  item.push( items.children().eq( i ) ); //item[0]=$('.item').eq(0)の要領で格納
  order.push( i );                       //order[0]=0の要領で数値を格納
  item[i].css('order', i );             //各アイテムのorderプロパティに値を振る
}

スライダーを動かした後に見かけ上の要素移動を行うための鍵が「index」。

indexの値が0からnumの間を行ったり来たりして、常にitem[index]が見かけ上の左端に位置するように処理していきます。

スライダーを動かす関数

スライダーを動かすための関数sliderを定義します。

function slider( prev ){ //引数prevで回す方向を指示
  running = true;        //アニメーション中のフラグON
  var offset = prev ? 0 : '-200%';
  items.animate({left: offset }, 600, 'swing', function(){
    ordering( prev );    //アニメーション後の並び替え処理の関数を実行
    running = false;     //アニメーションが終わったらフラグ解除
  });
}

引数prevはフリックとボタンの処理で出てきますが、逆回しの時(prevボタンを押した時)にtrueになるフラグを指しています。

流れは以下の通り。

  1. まずアニメーション中のフラグrunningの値をtrueにする
  2. prevの値によって、左右どっちに動かすか決める
  3. animateで動かす
  4. 動かし終わったら、アイテムの並べ替え処理をする(orderingを実行)
  5. runningフラグをfalseに戻す

次はorderingの定義です。

アイテムを並べ替える関数

スライダーを動かした後、アイテムの並び替えをする関数orderingを定義します。

いよいよindexを使って端っこのアイテムのorderプロパティを増やし(減らし)ます。

引数はさっきも出てきた、prevボタンを押した時にtrueになるprevです。

function ordering( prev ){
  if( prev ){ //prev == trueの時
    var i = index > 0 ? index - 1 : num - 1;
    item[i].css('order', order[i]-=num);
    index = i;
  } else {    //prev == falseまたはundefinedの時
    item[index].css('order', order[index]+=num);
    index = index < num - 1 ? index + 1 : 0;
  }
  items.css('left','-100%');
}

逆回し(prev == true)の時

アイテムの塊を右に動かした後の処理になるので、見かけ上で右端のアイテムを左端に持っていきたい。

たとえばアイテム4つの場合、今indexの値が0、つまりitem[0]が左端にいるとしたら、item[3]が右端なので、item[3]のorderの値を4減らせば、item[3]が一番前=左端に来ます。

左端にitem[3]が来たので、index = 3にし、同時にitemsのleftを-100%に戻します。

という処理を変数を使って行っているわけです。

通常回し(prev == false)の時

今度は逆の動きで、左端のアイテムを右端に持っていきます。

これは、左端にいるitem[index]のorderの値にnumを足してやればOK。

indexを足し(indexが最大なら、0にして)itemsのleftを-100%に戻します。

orderの値は足し算だけなので、スライダーが回り続ければどこまでも増え続けていきます。

何をやってるのか分からなくなってくるのが欠点

要素自体を移動させる普通のやり方と違って、orderを利用して見かけの並び順を変えるため、とても分かりづらいですね!

スライダーの自動再生

var setSlider = setInterval( slider, 5000);

sliderを5秒ごとに繰り返します。

この書き方だと引数にundefinedが入り、通常方向に回ります。

丁寧に書くならこうですね。

var setSlider = setInterval( function(){
  slider(false);
}, 5000);

フリックorクリック操作

ここまで来れば、あとは普通の処理だけです。

if('ontouchstart' in window){ // タッチデバイスかどうか判定
  // flick
  $('.buttons').remove();   // タッチデバイスならボタンを消す
  var startX,               // タッチ開始位置
      endX;                 // タッチ終了位置
  $('.slider').on('touchstart', function(e){
    startX = e.originalEvent.changedTouches[0].pageX;
    endX = startX;          // touchmoveが発生しないケースに備えてstartXを代入
  }).on('touchmove', function(e){
    endX = e.originalEvent.changedTouches[0].pageX;
  }).on('touchend', function(e){
    var difX = endX - startX;   // タッチ開始位置と終了位置の差分
    if( Math.abs(difX) > 100 ){ // 差分の絶対値|difX| > 100のときだけ実行
      if( running ) return;     // アニメーション中フラグがtrueなら処理を抜ける
      clearInterval( setSlider );         // 繰り返しを止める
      var prev = difX > 0 ? true : false; // difXの正負で回す向きを判定
      slider(prev);                       // sliderを1回実行
    }
  });
}else{
  // click
  $('.button').on('click', function(){
    if( running ) return;                          // フラグtrueなら処理を抜ける
    clearInterval( setSlider );                    // 繰り返しを止める
    var prev = $(this).is('.prev') ? true : false; // prevボタンclickならtrueを渡す
    slider( prev );                               // slider を1回実行
  });
}

これはコード中のコメント見れば分かりますね。

一度フリックかクリックすると、自動再生は停止したままになる仕様です。1回回した後にsetSliderを実行すれば、自動再生を再開させられます。

まとめ

いやぁ、説明するの難しかったー……

何はともあれ、CSSのみでスタイリングすることにより、ビューポートのサイズに合わせてレスポンシブにリサイズするスライダーを実現することができました。

最初のレンダリングもスムーズですし、resizeイベントを要素の幅を決め直す必要はありません。

問題はスライダーを動かした後の要素移動ですが、普通にappend/prepend使ったほうが分かりやすいとは思います。

でも、orderをいじって見た目の並びだけを変えるこの方法のほうが、非常に微妙ではあるけど処理が速いはず。

なので、そこはお好みで。

コメント

  1. […] > resizeイベント不要。最短コードでレスポンシブなスライダーを自作 […]