vanillaJSで簡易的なスライダー機能を実装したい
書こうと思った経緯
- 自分がカルーセル機能を素のJSで作ろうと思ったときに、ほとんどjQueryの解説記事ばかりで
まとまったサイトが見当たらなかったから。 - 同じようにvanillaJSでカルーセルを作りたい人が、いろいろ検索する手間を省けるようにしたいから。
- 自分の理解の整理のため。
実装している機能
- スライド送りボタンによる、カルーセルの動作
- ページネーション
- 一定時間ごとに、自動でカルーセルが動作する機能
完成図は以下
この記事に記載していること
- この記事は、https://pengi-n.co.jp/blog/carousel/ こちらの記事の手順に準じているため、
詳細の解説は上記記事に委ねます。 - 筆者の都合の良いように、上記記事から内容を一部改変しています。改変した部分は都度明記します。
- 上記記事はjQueryでの解説のため、JSへの書き換えに焦点を置いて記載します。
解説
全体像の確認
実際に解説に入る前に、全体の流れを確認します。
1.HTML/CSSで大枠を作成
2.ページ送りボタンを設置
3.ページ送りボタン機能の実装
4.ページネーションの実装
5.自動スライド送り機能の実装
順番に解説していきます。
HTML/CSSで大枠を作成
画像は任意のものを使用してください。
HTMLのコードは以下です。
<div class="carousel">
<ul class="carousel__area">
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-1.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-2.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-3.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-4.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-5.jpg" alt="" />
</li>
</ul>
</div>
SCSSのコードは以下です。フレックスボックスで画像を横並びにしています。
.carousel{
width: 600px;
height:calc(600px * 0.5625);
position: relative;
margin:0 auto;
@media all and (max-width:767px){
width:300px;
height:calc(300px * 0.5625);
}
//カルーセルのレイアウト
&__list{
width: 600px;
height:calc(600px * 0.5625);
margin-right: 30px;
@media all and (max-width:767px){
width:300px;
height:calc(300px * 0.5625);
margin-right: 0;
}
}
&__img{
width:100%;
height:100%;
object-fit: cover;
}
&__area{
position: absolute;
display: flex;
height:100%;
@media all and (max-width:767px){
width:1500px;
}
}
}
ページ送りボタンの設置
簡易的なものなので、アクセシビリティに配慮できていません。ご理解ください。
<div class="arrowWrap">
<div class="arrowWrap__left">
<button class="arrowWrap__btn js__btn-back"></button>
</div>
<div class="arrowWrap__right">
<button class="arrowWrap__btn js__btn-next"></button>
</div>
</div>
先ほどの[carousel]のdivにrelativeを指定して、[arrowWrap]を上下左右中央に設置しています。
.arrowWrap{
position: absolute;
top: 0;
left:50%;
transform: translateX(-50%);
width:90%;
height:100%;
display:flex;
justify-content:space-between;
align-items:center;
&__left,&__right{
background-color: rgba(113, 135, 245, 0.8);
width:48px;
height:48px;
display:flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
&__btn{
width:35%;
height:70%;
background-color: #fefefe;
display:block;
}
&__left{
.arrowWrap__btn{
clip-path: polygon(0 50%,100% 0,100% 100%);
}
}
&__right{
.arrowWrap__btn{
clip-path: polygon(0 0,100% 50%,0 100%);
}
}
}
ページ送りボタン機能の実装
1.関数の定義と実行
2.必要なDOMの取得と操作
3.スライドに番号を付与,変数化する
4.スライドの動きを関数化
5.ボタンを押したら関数実行
1.関数の定義と実行
window.addEventListener('DOMContentLoaded', function () {
carousel();//DOMの生成後に関数を実行
});
function carousel(){
この中に記載していく
}
2.必要なDOMの取得と操作
見にくいと思うので、HTMLも併記します
<div class="carousel">
<ul class="carousel__area">
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-1.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-2.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-3.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-4.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-5.jpg" alt="" />
</li>
</ul>
<div class="arrowWrap">
<div class="arrowWrap__left">
<button class="arrowWrap__btn js__btn-back"></button>
</div>
<div class="arrowWrap__right">
<button class="arrowWrap__btn js__btn-next"></button>
</div>
</div>
</div>
let slideLength = document.querySelectorAll('.carousel__list').length;//スライドの枚数
const slideList = document.querySelectorAll('.carousel__list');
let slideListStyle = getComputedStyle(slideList[0]);
let slideInterval = slideListStyle.getPropertyValue('margin-right');//スライド間の余白
let slideIntervalValueStr = slideInterval.replace('px', '');//スライド間の余白を”文字列”として取得
let slideIntervalValue = Number(slideIntervalValueStr);//スライド間の余白を数値へ変換
let slideWidth = document.querySelector('.carousel').clientWidth + slideIntervalValue;//スライドの幅+余白の幅
let slideIntervalTotal = slideIntervalValue * slideLength;//
let slideAreaWidth = slideWidth * slideLength + slideIntervalTotal + "px";//カルーセル全体の横幅
const slideArea = document.querySelector('.carousel__area');
slideArea.style.width = slideAreaWidth;//[.carousel__area]の横幅(width)を指定
const slideBackBtn = document.querySelector('.js__btn-back');
const slideNextBtn = document.querySelector('.js__btn-next');
参考サイトと違うのは、スライドの横幅に余白も足した部分です。
そうすることで、余白分だけスライドがずれていくことが無くなりました。
ポイント
outerWidth() => clientWidth
"要素の横幅の取得"
css() => style.property = '値'
"CSSの書き換え"
let slideInterval = slideListStyle.getPropertyValue('margin-right');//スライド間の余白
let slideIntervalValueStr = slideInterval.replace('px', '');//スライド間の余白を”文字列”として取得
let slideIntervalValue = Number(slideIntervalValueStr);//スライド間の余白を数値へ変換
let slideWidth = document.querySelector('.carousel').clientWidth + slideIntervalValue;//スライドの幅+余白の幅
余白(margin-right)の値を取得。[30px]が取得される。
replaceメソッドで[30px]から[px]を除去。
上記のみでは、[30]という"文字列"が取得されるので、スライドの幅+余白の幅の計算が
文字列の結合とみなされる
そのため、Numberメソッドにて数値への変換が必要。
3.スライドに番号を付与,変数化する
let slideCurrentIndex = 0;
let slideLastIndex = slideLength - 1;
4.スライドの動きを関数化
スライドの動きを[changeSlide]関数にまとめます。
function changeSlide() {
let slideMove = slideCurrentIndex * slideWidth;//スライドが移動する幅を指定
slideArea.style.willChange = 'transform';//[carousel__area]にwill-change:transform;を指定
slideArea.animate([
{ transform: `translateX(-${slideMove}px)` }
], {
duration: 500,
fill: 'forwards'
});
}
やっていること
- スライドが移動する幅を、[スライド番号 * スライドの幅]で定義
- そのままではかくついたので、will-changeを指定(改変箇所)
参考サイト:https://qiita.com/yuki153/items/9aac0e5c8d7230a7bbe2 - animateメソッドでスライドの移動を定義
参考サイト:https://note.affi-sapo-sv.com/js-dom-animate.php#tm
5.ボタンを押したら関数実行
slideNextBtn.addEventListener('click', function () {
if (slideCurrentIndex === slideLastIndex) {
slideCurrentIndex = 0;
changeSlide();
} else {
slideCurrentIndex++;
changeSlide();
}
});
slideBackBtn.addEventListener('click', function () {
if (slideCurrentIndex === 0) {
slideCurrentIndex = slideLastIndex;
changeSlide();
} else {
slideCurrentIndex--;
changeSlide();
}
})
[js__btn-next][js__btn-back]それぞれクリックイベントで[changeSlide]関数の実行。
ページネーションの実装
まずは、デザインの設定です。
<div class="carousel__pagination">
<span class="carousel__paginationCircle js-dot target"></span>
<span class="carousel__paginationCircle js-dot"></span>
<span class="carousel__paginationCircle js-dot"></span>
<span class="carousel__paginationCircle js-dot"></span>
<span class="carousel__paginationCircle js-dot"></span>
</div>
&__pagination{
width: 150px;
position: absolute;
bottom: 0;
left:50%;
transform: translate(-50%,200%);
display: flex;
justify-content:space-between;
}
&__paginationCircle{
width: 20px;
height:20px;
border:1px solid #333;
border-radius: 50%;
background-color: rgba(83, 97, 223, 0.3);
&.target{
background-color: rgba(83, 97, 223, 0.8);
}
}
次に機能の実装です。以下を[changeSlide]関数の中に追記します。
const paginationDot = document.querySelectorAll('.js-dot');
for (let i = 0; i < paginationDot.length; i++){
paginationDot[i].classList.remove('target');
}//一度全ての[js-dot]から[target]クラスを除去
paginationDot[slideCurrentIndex].classList.add('target');
//[スライド番号]番目の[js-dot]に[target]クラスを付与
pagenationDotはNodeListです。(以下、参考サイト)
自動スライド送り機能の実装
startTimer関数の中でTimer関数を定義します
let Timer;
function startTimer() {
Timer = setInterval(function () {
if (slideCurrentIndex === slideLastIndex) {
slideCurrentIndex = 0;
changeSlide();
} else {
slideCurrentIndex++;
changeSlide();
};
},3000);
};
このままでは永遠にTimer関数が動き続けるので、これを止める関数も定義します。
function stopTimer() {
clearInterval(Timer);
};
ページお送りボタンのクリックイベントの中で、stopTimer関数、startTimer関数の実行を追記します
slideNextBtn.addEventListener('click', function () {
stopTimer();
startTimer();
if (slideCurrentIndex === slideLastIndex) {
slideCurrentIndex = 0;
changeSlide();
} else {
slideCurrentIndex++;
changeSlide();
}
});
slideBackBtn.addEventListener('click', function () {
stopTimer();
startTimer();
if (slideCurrentIndex === 0) {
slideCurrentIndex = slideLastIndex;
changeSlide();
} else {
slideCurrentIndex--;
changeSlide();
}
});
完成したコードの確認
<div class="carousel">
<ul class="carousel__area">
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-1.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-2.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-3.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-4.jpg" alt="" />
</li>
<li class="carousel__list">
<img class="carousel__img" src="images/cafe-5.jpg" alt="" />
</li>
</ul>
<div class="arrowWrap">
<div class="arrowWrap__left">
<button class="arrowWrap__btn js__btn-back"></button>
</div>
<div class="arrowWrap__right">
<button class="arrowWrap__btn js__btn-next"></button>
</div>
</div>
<div class="carousel__pagination">
<span class="carousel__paginationCircle js-dot target"></span>
<span class="carousel__paginationCircle js-dot"></span>
<span class="carousel__paginationCircle js-dot"></span>
<span class="carousel__paginationCircle js-dot"></span>
<span class="carousel__paginationCircle js-dot"></span>
</div>
</div>
.carousel{
width: 600px;
height:calc(600px * 0.5625);
position: relative;
margin:0 auto;
@media all and (max-width:767px){
width:300px;
height:calc(300px * 0.5625);
}
//カルーセルのレイアウト
&__list{
width: 600px;
height:calc(600px * 0.5625);
margin-right: 30px;
@media all and (max-width:767px){
width:300px;
height:calc(300px * 0.5625);
margin-right: 0;
}
}
&__img{
width:100%;
height:100%;
object-fit: cover;
}
&__area{
position: absolute;
display: flex;
height:100%;
@media all and (max-width:767px){
width:1500px;
}
}
//ページネーションのレイアウト
&__pagination{
width: 150px;
position: absolute;
bottom: 0;
left:50%;
transform: translate(-50%,200%);
display: flex;
justify-content:space-between;
}
&__paginationCircle{
width: 20px;
height:20px;
border:1px solid #333;
border-radius: 50%;
background-color: rgba(83, 97, 223, 0.3);
&.target{
background-color: rgba(83, 97, 223, 0.8);
}
}
}
//スライド送りボタンのレイアウト
.arrowWrap{
position: absolute;
top: 0;
left:50%;
transform: translateX(-50%);
width:90%;
height:100%;
display:flex;
justify-content:space-between;
align-items:center;
&__left,&__right{
background-color: rgba(113, 135, 245, 0.8);
width:48px;
height:48px;
display:flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
&__btn{
width:35%;
height:70%;
background-color: #fefefe;
display:block;
}
&__left{
.arrowWrap__btn{
clip-path: polygon(0 50%,100% 0,100% 100%);
}
}
&__right{
.arrowWrap__btn{
clip-path: polygon(0 0,100% 50%,0 100%);
}
}
}
window.addEventListener('DOMContentLoaded', function () {
carousel();
})
function carousel() {
let slideLength = document.querySelectorAll('.carousel__list').length;//スライドの枚数
const slideList = document.querySelectorAll('.carousel__list');
let slideListStyle = getComputedStyle(slideList[0]);
let slideInterval = slideListStyle.getPropertyValue('margin-right');//スライド間の余白
let slideIntervalValueStr = slideInterval.replace('px', '');//スライド間の余白を”文字列”として取得
let slideIntervalValue = Number(slideIntervalValueStr);//スライド間の余白を数値へ変換
let slideWidth = document.querySelector('.carousel').clientWidth + slideIntervalValue;//スライドの幅+余白の幅
let slideIntervalTotal = slideIntervalValue * slideLength;//
let slideAreaWidth = slideWidth * slideLength + slideIntervalTotal + "px";//カルーセル全体の横幅
const slideArea = document.querySelector('.carousel__area');
slideArea.style.width = slideAreaWidth;//[.carousel__area]の横幅(width)を指定
const slideBackBtn = document.querySelector('.js__btn-back');
const slideNextBtn = document.querySelector('.js__btn-next');
let slideCurrentIndex = 0;
let slideLastIndex = slideLength - 1;
function changeSlide() {
let slideMove = slideCurrentIndex * slideWidth;
slideArea.style.willChange = 'transform';
slideArea.animate([
{ transform: `translateX(-${slideMove}px)` }
], {
duration: 500,
fill: 'forwards'
});
//ページネーションの変数を定義
const paginationDot = document.querySelectorAll('.js-dot');
for (let i = 0; i < paginationDot.length; i++){
paginationDot[i].classList.remove('target');
}
paginationDot[slideCurrentIndex].classList.add('target');
};
let Timer;
function startTimer() {
Timer = setInterval(function () {
if (slideCurrentIndex === slideLastIndex) {
slideCurrentIndex = 0;
changeSlide();
} else {
slideCurrentIndex++;
changeSlide();
};
},3000);
};
function stopTimer() {
clearInterval(Timer);
};
startTimer();
slideNextBtn.addEventListener('click', function () {
stopTimer();
startTimer();
if (slideCurrentIndex === slideLastIndex) {
slideCurrentIndex = 0;
changeSlide();
} else {
slideCurrentIndex++;
changeSlide();
}
});
slideBackBtn.addEventListener('click', function () {
stopTimer();
startTimer();
if (slideCurrentIndex === 0) {
slideCurrentIndex = slideLastIndex;
changeSlide();
} else {
slideCurrentIndex--;
changeSlide();
}
});
}
今後実装したい機能
- 無限ループ(最後のスライドに行ったら、ぎゅんって最初に戻らないやつ)
- ページネーションクリックしてスライド移動できるやつ
- ページネーションのドットを、スライド枚数で自動生成する
- スライド表示枚数の変更
- スライド移動の時のアニメーション
- スワイプ対応
最後に
意味わからないところ、間違っているところはコメント頂けると嬉しいです。
vanillaJSにわかなので、コードが冗長かもしれません。
今回の経験で、基本的なカルーセルを実装できたので
今後はもっと機能を増やして行きたいと思います。
P.S
誰か、一周目に謎にぎゅんってなるバグの原因教えてください。。。
Discussion