簡易的なスライダーを実装する

サイト構築時にスライダー処理が必要な際はライブラリを使用することが多いですが、ライブラリを使用するほどでもない軽微なスライダーも度々あるので、使いまわせるような簡易的なスライダーを実装してみます。

サンプルコード

今回はフェードで切り替わるスライダーと横スライドで切り替わるスライダーの2種類を作ります。

フェードで切り替わるスライダー

フェードで切り替わるスライダーは以下のような機能で実装してみます。

  • 前後ボタンでの切り替え
  • スライド要素の枚数分のドットを表示
  • ドットをクリックで指定のスライド要素に切り替え
  • 一定時間後に次のスライド要素に自動切り替え
  • スライドのループ
  • スライダーの複数設置の対応

まずはHTMLです。

<div class="slider js-slider">
  <div class="slider_list">
    <div class="slider_item js-slider-item is-show">Slide1</div>
    <div class="slider_item js-slider-item">Slide2</div>
    <div class="slider_item js-slider-item">Slide3</div>
    <div class="slider_item js-slider-item">Slide4</div>
    <div class="slider_item js-slider-item">Slide5</div>
    <div class="slider_item js-slider-item">Slide6</div>
  </div>
  <button class="slider_prev js-slider-prev">Previous</button>
  <button class="slider_next js-slider-next">Next</button>
</div>

構造的にはよくあるスライダーの作りで、JavaScriptで制御の入る部分には「js」から始まるclassを付与しています。

次にCSSです。
必要な部分のみ抜粋しているので、全体はデモページをご確認ください。

.slider {
	position: relative;
	width: 640px;
	margin: auto;
}
.slider_list {
	position: relative;
}
.slider_item {
	position: relative;
	width: 100%;
	opacity: 0;
	transition: opacity 600ms;
}
.slider_item:not(:first-child) {
	position: absolute;
	top: 0;
	left: 0;
}
.slider_item.is-show {
	z-index: 5;
	opacity: 1;
}

フェードでの切り替えのため各スライド要素をpositionで重ねた上で、is-showのclassが付与された要素を重なり順の最前にくるようにしています。

最後にJavaScriptです。

const sliderStatus = []; // 各スライダーの状態管理用
const sliderOptions = {
	slider: 'js-slider',
	slideItem: 'js-slider-item',
	slidePrev: 'js-slider-prev',
	slideNext: 'js-slider-next',
	slideDot: 'js-slider-dot',
	currentClass: 'is-show',
	dotCurrentClass: 'is-active',
	slideSpeed: 600,
	autoSpeed: 3000,
}

window.addEventListener('load', function() {
	const sliders = document.querySelectorAll('.'+sliderOptions['slider']);
	for (let i = 0; i < sliders.length; i++) {
		slider_init(i);
	}
});

/**
 * スライダーの初期設定
 * @param {number} sliderNo - 対象のスライダー番号
 */
function slider_init(sliderNo) {
	const slider = document.querySelectorAll('.'+sliderOptions['slider'])[sliderNo];
	const slideItems = slider.querySelectorAll('.'+sliderOptions['slideItem']);
	// スライド枚数が0枚の場合は処理を行わない
	if(slideItems.length <= 0) return;
	sliderStatus.push({
		count: 0,
		autoTimer: ''
	});
	generate_dots(sliderNo);
	slider.querySelector('.'+sliderOptions['slidePrev']).addEventListener('click', function() {
		prev_slide(sliderNo);
	});
	slider.querySelector('.'+sliderOptions['slideNext']).addEventListener('click', function() {
		next_slide(sliderNo);
	});
	// 次の自動切り替えのタイマー設定
	sliderStatus[sliderNo]['autoTimer'] = setTimeout(function() {
		next_slide(sliderNo);
	}, sliderOptions['autoSpeed']);
}

/**
 * スライダーのドット生成
 * @param {number} sliderNo - 対象のスライダー番号
 */
function generate_dots(sliderNo) {
	const slider = document.querySelectorAll('.'+sliderOptions['slider'])[sliderNo];
	// ドットのタグ生成と追加
	let wrap = document.createElement('div');
	wrap.classList.add('slider_dots');
	let dots = '';
	const slideItems = slider.querySelectorAll('.'+sliderOptions['slideItem']);
	for (let i = 0; i < slideItems.length; i++) {
		dots += '<button class="slider_dot '+sliderOptions['slideDot']+'">'+i+'</button>';
	}
	wrap.innerHTML = dots;
	slider.appendChild(wrap);

	// 各ドットにイベントを設定
	const slideDots = slider.querySelectorAll('.'+sliderOptions['slideDot']);
	slideDots[sliderStatus[sliderNo]['count']].classList.add(sliderOptions['dotCurrentClass']);
	for (let i = 0; i < slideDots.length; i++) {
		slideDots[i].addEventListener('click', {
			sliderNnum: sliderNo,
			itemNum: i,
			handleEvent: go_slide
		});
	}
}

/**
 * スライダーのスライド処理
 * @param {number} sliderNo - 対象のスライダー番号
 */
function slider_slide(sliderNo) {
	const slider = document.querySelectorAll('.'+sliderOptions['slider'])[sliderNo];
	// カレントクラスの付け替え
	slider.querySelector('.'+sliderOptions['slideItem']+'.'+sliderOptions['currentClass']).classList.remove(sliderOptions['currentClass']);
	slider.querySelectorAll('.'+sliderOptions['slideItem'])[sliderStatus[sliderNo]['count']].classList.add(sliderOptions['currentClass']);
	// ドットの状態更新
	dot_update(sliderNo);
	// フェードのアニメーション終了後、スライド状態監視用のclass除去
	setTimeout(function() {
		slider.classList.remove('is-sliding');
	}, sliderOptions['slideSpeed']);
	// 次の自動切り替えのタイマー設定
	sliderStatus[sliderNo]['autoTimer'] = setTimeout(function() {
		next_slide(sliderNo);
	}, sliderOptions['autoSpeed']);
}

/**
 * 1つ前のスライドに移動する処理
 * @param {number} sliderNo - 対象のスライダー番号
 */
function prev_slide(sliderNo) {
	const slider = document.querySelectorAll('.'+sliderOptions['slider'])[sliderNo];
	// スライド中の場合は処理を行わない(連打防止)
	if(slider.classList.contains('is-sliding')) return;
	// スライド状態監視用のclass付与
	slider.classList.add('is-sliding');
	// 自動切り替えのタイマー解除
	clearTimeout(sliderStatus[sliderNo]['autoTimer']);
	// 次に表示するスライドを設定してスライド処理開始
	sliderStatus[sliderNo]['count']--;
	if(sliderStatus[sliderNo]['count'] < 0) sliderStatus[sliderNo]['count'] = slider.querySelectorAll('.'+sliderOptions['slideItem']).length - 1;
	slider_slide(sliderNo);
}

/**
 * 1つ先のスライドに移動する処理
 * @param {number} sliderNo - 対象のスライダー番号
 */
function next_slide(sliderNo) {
	const slider = document.querySelectorAll('.'+sliderOptions['slider'])[sliderNo];
	// スライド中の場合は処理を行わない(連打防止)
	if(slider.classList.contains('is-sliding')) return;
	// スライド状態監視用のclass付与
	slider.classList.add('is-sliding');
	// 自動切り替えのタイマー解除
	clearTimeout(sliderStatus[sliderNo]['autoTimer']);
	// 次に表示するスライドを設定してスライド処理開始
	sliderStatus[sliderNo]['count']++;
	if(sliderStatus[sliderNo]['count'] > slider.querySelectorAll('.'+sliderOptions['slideItem']).length - 1) sliderStatus[sliderNo]['count'] = 0;
	slider_slide(sliderNo);
}

/**
 * 指定したスライドに移動する処理
 */
function go_slide() {
	const slider = document.querySelectorAll('.'+sliderOptions['slider'])[this.sliderNnum];
	// スライド中の場合は処理を行わない(連打防止)
	if(slider.classList.contains('is-sliding')) return;
	// スライド状態監視用のclass付与
	slider.classList.add('is-sliding');
	// 自動切り替えのタイマー解除
	clearTimeout(sliderStatus[this.sliderNnum]['autoTimer']);
	// 次に表示するスライドを設定してスライド処理開始
	sliderStatus[this.sliderNnum]['count'] = this.itemNum;
	slider_slide(this.sliderNnum);
}

/**
 * スライダーのドットの更新
 * @param {number} sliderNo - 対象のスライダー番号
 */
function dot_update(sliderNo) {
	const slider = document.querySelectorAll('.'+sliderOptions['slider'])[sliderNo];
	// ドットのカレントクラスの付け替え
	slider.querySelector('.'+sliderOptions['slideDot']+'.'+sliderOptions['dotCurrentClass']).classList.remove(sliderOptions['dotCurrentClass']);
	slider.querySelectorAll('.'+sliderOptions['slideDot'])[sliderStatus[sliderNo]['count']].classList.add(sliderOptions['dotCurrentClass']);
}

大まかにはコード内のコメントに記載している通りです。
フェードで切り替わるスライダーのデモページ

横スライドで切り替わるスライダー

横スライドで切り替わるスライダーは以下の機能になります。
フェードであったドットに関してはややこしくなるため、こちらの機能からは除外しています。

  • 前後ボタンでの切り替え
  • 一定時間後に次のスライド要素に自動切り替え
  • スライドのループ
  • スライダーの複数設置の対応

HTMLはフェードの場合から少し構造を変更しています。

<div class="slider js-slider">
  <div class="slider_wrapper">
    <div class="slider_list js-slider-list">
      <div class="slider_item--1 js-slider-item">Slide1</div>
      <div class="slider_item--2 js-slider-item">Slide2</div>
      <div class="slider_item--3 js-slider-item">Slide3</div>
      <div class="slider_item--4 js-slider-item">Slide4</div>
      <div class="slider_item--5 js-slider-item">Slide5</div>
      <div class="slider_item--6 js-slider-item">Slide6</div>
    </div>
  </div>
  <button class="slider_prev js-slider-prev">Previous</button>
  <button class="slider_next js-slider-next">Next</button>
</div>

主な変更点としては下記になります。

  • .slider_wrapperの要素追加。
  • .slider_listに「js」始まりのclass追加。(この要素がスライドを行うようになるため)

次にCSSです。
フェードの時と同じく、必要な部分のみ抜粋しています。

.slider {
	position: relative;
	width: 640px;
	margin: auto;
}
.slider_wrapper {
	overflow: hidden;
}
.slider_list {
	display: flex;
}
.slider_item,
.slider_item--1,
.slider_item--2,
.slider_item--3,
.slider_item--4,
.slider_item--5,
.slider_item--6 {
	flex-shrink: 0;
	width: 100%;
}

スライドで切り替えのため display: flex; とflex-shrink: 0; で横に並べた上で、overflow: hidden;で範囲外のスライド要素は表示されないようにしています。

最後にJavaScriptです。

const sliderStatus = []; // 各スライダーの状態管理用
const sliderOptions = {
	slider: 'js-slider',
	slideList: 'js-slider-list',
	slideItem: 'js-slider-item',
	slidePrev: 'js-slider-prev',
	slideNext: 'js-slider-next',
	slideSpeed: 600,
	autoSpeed: 3000,
}

window.addEventListener('load', function() {
	const sliders = document.querySelectorAll('.'+sliderOptions['slider']);
	for (let i = 0; i < sliders.length; i++) {
		slider_init(i);
	}
});

/**
 * スライダーの初期設定
 * @param {number} sliderNo - 対象のスライダー番号
 */
function slider_init(sliderNo) {
	const slider = document.querySelectorAll('.'+sliderOptions['slider'])[sliderNo];
	const slideItems = slider.querySelectorAll('.'+sliderOptions['slideItem']);
	// スライド枚数が1枚以下の場合は処理を行わない
	if(slideItems.length <= 1) return;
	sliderStatus.push({
		autoTimer: ''
	})
	slider.querySelector('.'+sliderOptions['slidePrev']).addEventListener('click', function() {
		slider_slide(sliderNo, -1);
	});
	slider.querySelector('.'+sliderOptions['slideNext']).addEventListener('click', function() {
		slider_slide(sliderNo, 1);
	});
	// 次の自動切り替えのタイマー設定
	sliderStatus[sliderNo]['autoTimer'] = setTimeout(function() {
		slider_slide(sliderNo, 1);
	}, sliderOptions['autoSpeed']);
}

/**
 * スライダーのスライド処理
 * @param {number} sliderNo  - 対象のスライダー番号
 * @param {number} direction - スライドの方向
 */
function slider_slide(sliderNo, direction) {
	const slider = document.querySelectorAll('.'+sliderOptions['slider'])[sliderNo];
	const sliderWrap = slider.querySelector('.'+sliderOptions['slideList']);
	const slideItems = slider.querySelectorAll('.'+sliderOptions['slideItem']);
	const slideWidth = sliderWrap.clientWidth;
	// スライド中の場合は処理を行わない(連打防止)
	if(sliderWrap.classList.contains('is-sliding')) return;
	// スライド状態監視用のclass付与
	sliderWrap.classList.add('is-sliding');
	clearTimeout(sliderStatus[sliderNo]['autoTimer']);
	// 右方向に進む場合
	if(direction === 1) {
		// スライドアニメーションの設定
		sliderWrap.style.transition = 'transform '+sliderOptions['slideSpeed']+'ms';
		sliderWrap.style.transform = 'translate3d(-'+slideWidth+'px, 0px, 0px)';
		// スライドのアニメーション終了後
		setTimeout(function() {
			// 先頭のスライドを末尾に移動
			sliderWrap.appendChild(slideItems[0]);
			// アニメーションの設定を初期化
			sliderWrap.style.transition = '';
			sliderWrap.style.transform = '';
			// スライド状態監視用のclass除去
			sliderWrap.classList.remove('is-sliding');
		}, sliderOptions['slideSpeed']);
	// 左方向に進む場合
	} else if(direction === -1) {
		// 末尾のスライドを先頭に移動
		sliderWrap.insertBefore(slideItems[slideItems.length-1], slideItems[0]);
		// スライドのアニメーション開始前の設定
		sliderWrap.style.transform = 'translate3d(-'+slideWidth+'px, 0px, 0px)';
		// アニメーション開始前の設定から1ms後にアニメーション処理実行
		setTimeout(function() {
			// スライドアニメーションの設定
			sliderWrap.style.transition = 'transform '+sliderOptions['slideSpeed']+'ms';
			sliderWrap.style.transform = 'translate3d(0px, 0px, 0px)';
		}, 1);
		// スライドのアニメーション終了後
		setTimeout(function() {
			// アニメーションの設定を初期化
			sliderWrap.style.transition = '';
			sliderWrap.style.transform = '';
			// スライド状態監視用のclass除去
			sliderWrap.classList.remove('is-sliding');
		}, sliderOptions['slideSpeed']);
	}
	// 次の自動切り替えのタイマー設定
	sliderStatus[sliderNo]['autoTimer'] = setTimeout(function() {
		slider_slide(sliderNo, 1);
	}, sliderOptions['autoSpeed']);
}

フェードの時とは異なり、横に並んだスライダー全体がスライドして動く形になるのと、右端までスライドしてしまうと次のスライド要素がなくなってしまうため、特定のタイミングでスライド要素を移動させたりする必要があります。
そのため、classの付け替えによってスライドを行うのではなく、DOMの操作(style属性への設定やスライド要素の順番の変更など)でスライドするようにしています。
横スライドで切り替わるスライダーのデモページ

このエントリーをはてなブックマークに追加

関連記事

コメントを残す

メールアドレスが公開されることはありません。
* が付いている欄は必須項目です

CAPTCHA


コメントが承認されるまで時間がかかります。

2024年10月
 12345
6789101112
13141516171819
20212223242526
2728293031