目次
カルーセルとは
カルーセル(Carousel)は、複数の画像やテキストが円環状に配置され、ユーザーが前後にスライドして次のコンテンツを表示できるようなUIデザインのことを指します。
カルーセルはJavaScriptライブラリを使用すれば簡単に作成できますが、ライブラリだと細かい箇所の調整がしづらく、余計なコードが追加されるデメリットがあります。
そのため自作したほうがコードの自由度は高くなりますが、JavaScript初心者がライブラリを使わずに1から作成するのは難しいです。
しかし、この記事内に書いた作成方法であれば初心者でも簡単にカルーセルを作成できます。
※ 記事内に書かれているCSS、配列、DOM操作などが理解できない場合は、まだ初心者の域に達していないので、CSSやJavaScriptの基礎をしっかり学んでから記事を見ることをオススメします。
カルーセル or スライダー
ちなみにこのようなUIをカルーセルではなくスライダーと呼称する人もいます。
しかし、スライダーだと範囲指定のスライダーと混同して混乱するので、スライダーと呼称するのはやめたほうが良いです。
範囲指定のスライダーとは以下のようなUIのものです。
カルーセルの英語のスペルはcarousel
カルーセルの英語のスペルはcarouselです。
carouselは日本語では回転木馬(メリーゴーランド)という意味です。
ちなみに空港などで見かける荷物がぐるぐる回っているやつは手荷物カルーセル(Baggage carousel)と呼ばれています。
caroucelと「s」を「c」にしてしまうスペルミスが多いので、コードを書く際は注意が必要です。
カルーセルのHTMLを作成
まず例としてHTMLを以下のような構造で作成します。
画像は3枚用意して、「←」と「→」はボタンを押すことで左右に遷移されるボタンです。
<div class="carousel-outer">
<div class="carousel">
<div class="carousel__section">
<img src="https://placehold.jp/ccffff/000000/960x540.png?text=Section1">
</div>
<div class="carousel__section">
<img src="https://placehold.jp/ffccff/000000/960x540.png?text=Section2">
</div>
<div class="carousel__section">
<img src="https://placehold.jp/ccffcc/000000/960x540.png?text=Section3">
</div>
</div>
<button class="carousel__button-left">←</button>
<button class="carousel__button-right">→</button>
</div>
カルーセルのCSSを作成
次にCSSを以下のように作成します。
.carousel-outer に外枠の幅を指定して、この幅を超える場合(画像の2枚目以降)はoverflow: hidden; で非表示にします。
また、各画像をflexで横1列に表示して、左右に遷移するボタンをposition: absolute; で配置しています。
カルーセルの画像の移動は.carouselを「translateX(0)」を「translateX(-100%)」、「translateX(-200%)」と変化させることで遷移させます。
※ transformではなく、translateを直接使ったほうがコードを短くできますが、現時点では認知度が低いのでtransformを使用しています。
img {
max-width: 100%;
height: auto;
}
.carousel-outer {
position: relative;
overflow: hidden;
width: 100%;
max-width: 960px;
}
.carousel {
display: flex;
transform: translateX(0);
}
.carousel__button-left,
.carousel__button-right {
position: absolute;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
}
.carousel__button-left {
left: 5%;
}
.carousel__button-right {
right: 5%;
}
HTMLとCSSで作成したカルーセルは以下のようになります。
まだJavaScriptのコードを追加していないので、ボタンを押しても動作しません。
カルーセルのJavaScriptを作成
最後にカルーセルを動かすためのJavaScriptを次のように作成します。
「←」または「→」ボタンを押すことでtranslateXの値が変わって、1枚目、2枚目…のように遷移します。
let current = 0
const carousel = document.querySelector('.carousel')
const carousels = document.querySelectorAll('.carousel__section')
const total = carousels.length
function showSection() {
carousel.style.transform = `translateX(${-current * 100}%)`
}
function prevSection() {
current = current === 0 ? total - 1 : current - 1
showSection()
}
function nextSection() {
current = (current + 1) % total
showSection()
}
const btnLeft = document.querySelector('.carousel__button-left')
const btnRight = document.querySelector('.carousel__button-right')
btnLeft.addEventListener('click', prevSection)
btnRight.addEventListener('click', nextSection)
これらのコードで作成したカルーセルがこちらです。
アニメーションでスムーズに動かす
一般的なカルーセルはスムーズなアニメーションにより左右に遷移します。
しかし、前述のコードではカルーセルはスムーズなアニメーションになっていません。
これは.carouselにtransitionプロパティが付いていないからです。
transition: transform 0.5s ease-in-out; を追記することでスムーズなアニメーションになります。
.carousel {
display: flex;
transform: translateX(0);
transition: transform 0.5s ease-in-out; /* ← 追加 */
}
アニメーションを無限スクロールにする
前述のカルーセルのサンプルだと3つ目まで遷移してさらに「→」を押すと右から左にスクロールして1つ目に戻ります。
なぜなら3つ目から右の要素は存在しないからです。
① ② ③
しかし、一般的なアニメーションのカルーセルだと3つ目からさらに「→」を押すと1つ目があらわれます。
これは3つ目の右に1つ目が複製されて配置されており、これを表示後は複製元の1つ目の位置にアニメーションなしで戻っているからです。
「←」を押した場合は「→」とは逆の処理になります。
図にすると以下のようになっています。
このように最後と最初の要素が複製されてこのように配置されている。 (| | の内側は表示されている範囲) ③ | ① | ② ③ ① → を2回押すとこうなる。 ③ ① ② | ③ | ① さらに → を押すと複製した①が表示される ③ ① ② ③ | ① | 複製した①表示後に複製元の①に「アニメーションなし」で戻される。 ③ |①| ② ③ ①
アニメーションの有無は「.no-transition」をという transition: none; が適用されるCSSクラスを作成して、アニメーションの有無を切り替え可能にします。
.no-transition {
transition: none;
}
無限スクロールのカルーセルの処理をJavaScriptで書くと以下のようになります。
「③ | ① | ② ③ ①」のように要素開始位置が[0]から[1]に変わっているので、開始位置の変数の値(current)などが変更されています。
また、アニメーション付きのカルーセルの場合はアニメーション中に連続して「←」や「→」を押されると動きがおかしくなるので、isAnimationというアニメーション中か判定するフラグ用の変数を追加しています。
アニメーション中はtrueにして、trueの場合は「←」や「→」を押されても、returnして処理されないようにしています。
transitionendイベントでカルーセルのアニメーションの終了を検知して、isAnimationをtrueからfalseに戻しています。
let current = 1
let isAnimation = false;
const carousel = document.querySelector('.carousel')
const carousels = document.querySelectorAll('.carousel__section')
const carouselLen = carousels.length
const lastCarousel = carouselLen + 1
const total = carouselLen + 2;
const newOrder = [];
newOrder.push(carousels[carouselLen - 1].outerHTML)
carousels.forEach((section) => {
newOrder.push(section.outerHTML)
})
newOrder.push(carousels[0].outerHTML)
carousel.innerHTML = newOrder.join('')
function showSection() {
isAnimation = true;
carousel.style.transform = `translateX(${-current * 100}%)`;
}
function prevSection() {
if (isAnimation) return
current = current === 0 ? carouselLen + 1 : current - 1;
showSection()
}
function nextSection() {
if (isAnimation) return
current = (current + 1) % (carouselLen + 2);
showSection()
}
const btnLeft = document.querySelector('.carousel__button-left')
const btnRight = document.querySelector('.carousel__button-right')
btnLeft.addEventListener('click', prevSection)
btnRight.addEventListener('click', nextSection)
carousel.addEventListener('transitionend', () => {
if (current === carouselLen + 1) {
current = 1
} else if (current === 0) {
current = carouselLen
}
carousel.classList.add('no-transition')
carousel.style.transform = `translateX(${-current * 100}%)`
setTimeout(() => {
carousel.classList.remove('no-transition')
isAnimation = false
}, 20)
})
キーボードでの操作を可能にする
「←」や「→」を押すことで「前へ」「次へ」の操作になっていますが、表示される要素がたくさんある場合はカーソルをマウスやトラックパッドで合わせて、何度も押して操作するのは大変です。
例えば「よう実のキャラクター紹介」のページは、この記事の最初のサンプルのようなUIになっていますが、キャラクター数(要素数)が31なので「←」や「→」でキーボード操作ができたほうが利便性が高いです。
JavaScriptで何のキーボードが押されたかは「keydownイベント」で検知できます。
以下のようにevent.keyが「ArrowLeft」または「ArrowRight」のときに、すでに作成している prevSection() または nextSection() を実行することでキーボード操作を実装できます。
function handleKeyDown(event) {
if (isAnimation) return
if (event.key === 'ArrowLeft') {
prevSection()
} else if (event.key === 'ArrowRight') {
nextSection()
}
}
document.addEventListener('keydown', handleKeyDown)
インジケーター(●◯◯)を表示する
カルーセルには現在の位置を示すためのインジケーター(●◯◯)がカルーセルに表示されていることがあります。
インジケーター(●◯◯)はHTMLとCSSで作成して、JavaScriptで現在位置によってインジケーター(●◯◯)の見た目を要素のindexの位置で判定して、(◯●◯)などに変化させるだけで実装できます。
function updateIndicators(newIndex) {
if (newIndex < 0) {
newIndex = carouselLen - 1
} else if (newIndex > (carouselLen - 1)) {
newIndex = 0
}
document.querySelectorAll('.carousel__indicator > li').forEach((dot, index) => {
dot.classList.toggle('active', index === newIndex)
})
}
function showSection() {
isAnimation = true;
carousel.style.transform = `translateX(${-current * 100}%)`;
updateIndicators(current - 1)
}
表示数が動的に変化して1や0になる場合の処理
カルーセルのHTMLの表示数が動的に変化して、表示数が1や0の可能性もあるのであれば、JavaScriptのコードにkeydownイベント除去の処理と、1の場合は「ボタンとインジゲーターを消す」、0の場合は「カルーセル自体を消す」という処理の追加が必要です。
function checkCarouselSectionLen() {
if (carouselLen === 1) {
btnLeft.style.display = 'none'
btnRight.style.display = 'none'
document.querySelector('.carousel__indicator').style.display = 'none'
document.removeEventListener('keydown', handleKeyDown)
} else if (carouselLen === 0) {
carousel.style.display = 'none'
document.removeEventListener('keydown', handleKeyDown)
}
}
checkCarouselSectionLen()
カルーセルは表示数に注意が必要
この記事で作成したサンプルのカルーセルは3画像の切り替えになっていますが、CSSとJavaScriptは増減しても問題ないように作成してありますので、以下のようにHTMLを6画像にしても動作します。
<div class="carousel">
<div class="carousel__section">
<img src="https://placehold.jp/ccffff/000000/960x540.png?text=Section1">
</div>
<div class="carousel__section">
<img src="https://placehold.jp/ffccff/000000/960x540.png?text=Section2">
</div>
<div class="carousel__section">
<img src="https://placehold.jp/ccffcc/000000/960x540.png?text=Section3">
</div>
<div class="carousel__section">
<img src="https://placehold.jp/aaffff/000000/960x540.png?text=Section4">
</div>
<div class="carousel__section">
<img src="https://placehold.jp/ffaaff/000000/960x540.png?text=Section5">
</div>
<div class="carousel__section">
<img src="https://placehold.jp/aaffaa/000000/960x540.png?text=Section6">
</div>
</div>
しかし、表示数が多すぎると必然的に最後のほうは見られる機会が少なくなりますし、最後まで切り替えるのも時間がかかるようになります。
そのため、Webサイトでカルーセルを使用する際に表示数が多くなりそうな場合は、表示数は最大6個までにするなどのルールを作成して運用したほうが良いです。