Building an Auto-Playing Image Carousel in Vue.js
Core reqiurements: images cycle automatically on page load. The slideshow pauses when the mouse hovers over the gallery and resumes when the mouse leaves. Users can navigate between slides using left/right arrow buttons. Indicator dots at the bottom reflect the currently active image.
Template Structure
<template>
<div class="gallery-wrapper">
<div class="viewport" @mouseenter="pauseSlide" @mouseleave="resumeSlide">
<ul class="slide-track" :style="trackTransform">
<li>
<img :style="{width: slideWidth + 'px'}" :src="images[images.length - 1].src" alt="">
</li>
<li v-for="(img, idx) in images" :key="idx">
<img :style="{width: slideWidth + 'px'}" :src="img.src" alt="">
</li>
<li>
<img :style="{width: slideWidth + 'px'}" :src="images[0].src" alt="">
</li>
</ul>
<ul class="controls">
<li class="btn-prev" @click="shiftSlide(500, 1, velocity)">
<svg class="arrow-icon" width="30px" height="30px" viewBox="0 0 1024 1024">
<path fill="#ffffff" d="M481.233 904c8.189 0 16.379-3.124 22.628-9.372 12.496-12.497 12.496-32.759 0-45.256L166.488 512l337.373-337.373c12.496-12.497 12.496-32.758 0-45.255-12.498-12.497-32.758-12.497-45.256 0l-360 360c-12.496 12.497-12.496 32.758 0 45.255l360 360c6.249 6.249 14.439 9.373 22.628 9.373z" />
</svg>
</li>
<li class="btn-next" @click="shiftSlide(500, -1, velocity)">
<svg class="arrow-icon" width="30px" height="30px" viewBox="0 0 1024 1024">
<path fill="#ffffff" d="M557.179 904c-8.189 0-16.379-3.124-22.628-9.372-12.496-12.497-12.496-32.759 0-45.256L871.924 512 534.551 174.627c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0l360 360c12.496 12.497 12.496 32.758 0 45.255l-360 360c-6.249 6.249-14.439 9.373-22.628 9.373z" />
</svg>
</li>
</ul>
<ul class="indicators">
<li v-for="(dot, i) in images" :key="i"
:class="{ active: i === (activeIdx - 1) }"
@click="goToSlide(i + 1)">
</li>
</ul>
</div>
</div>
</template>
Styling
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
ol, ul {
list-style: none;
}
.gallery-wrapper {
text-align: center;
}
.viewport {
position: relative;
width: 500px;
height: 350px;
margin: 0 auto;
overflow: hidden;
}
.slide-track {
display: flex;
position: absolute;
}
.btn-prev, .btn-next {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 50%;
cursor: pointer;
}
.btn-prev {
left: 3%;
padding-left: 12px;
padding-top: 10px;
}
.btn-next {
right: 3%;
padding-right: 12px;
padding-top: 10px;
}
img {
user-select: none;
}
.indicators {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
}
.indicators li {
display: inline-block;
width: 15px;
height: 15px;
margin: 0 3px;
border: 1px solid white;
border-radius: 50%;
background-color: #333;
cursor: pointer;
}
.indicators .active {
background-color: orange;
}
Componant Logic
<script>
export default {
name: 'ImageCarousel',
props: {
baseSpeed: {
type: Number,
default: 25
},
timing: {
type: Number,
default: 3
}
},
data() {
return {
images: [
{ src: 'http://img.hb.aicdn.com/adbde61e4343dedd21e97ea7f22666825a8db7d077ffe-qn8Pjn_fw658' },
{ src: 'http://img.hb.aicdn.com/adeed7d28df6e776c2fa6032579c697381d1a82b7fe00-fwRqgn_fw658' },
{ src: 'http://img.hb.aicdn.com/ab7f48509b3c0353017d9a85ef1d12400c9b2724540d4-p3zouo_fw658' },
{ src: 'http://img.hb.aicdn.com/60f788fc2a846192f224b9e6d4904b30e54926211d3d67-ACFJ9G_fw658' },
{ src: 'http://img.hb.aicdn.com/22ded455284aab361b8d2056e82f74a891a019704296a-PSraEB_fw658' }
],
slideWidth: 500,
activeIdx: 1,
offset: -500,
canAnimate: true,
velocity: this.baseSpeed
}
},
computed: {
trackTransform() {
return {
transform: `translate3d(${this.offset}px, 0, 0)`
}
},
cycleTime() {
return this.timing * 1000
}
},
mounted() {
this.startAutoPlay()
window.onblur = () => this.pauseSlide()
window.onfocus = () => this.startAutoPlay()
},
methods: {
startAutoPlay() {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
this.intervalId = setInterval(() => {
this.shiftSlide(500, -1, this.velocity)
}, this.cycleTime)
},
pauseSlide() {
clearInterval(this.intervalId)
this.intervalId = null
},
resumeSlide() {
this.startAutoPlay()
},
shiftSlide(step, dir, speed) {
if (!this.canAnimate) return
this.canAnimate = false
dir === -1 ? this.activeIdx += step / 500 : this.activeIdx -= step / 500
if (this.activeIdx > 5) this.activeIdx = 1
if (this.activeIdx < 1) this.activeIdx = 5
const targetPos = this.offset + step * dir
this.smoothScroll(targetPos, dir, speed)
},
smoothScroll(target, direction, speed) {
if (this.animFrame) {
clearInterval(this.animFrame)
this.animFrame = null
}
this.animFrame = setInterval(() => {
if ((direction === -1 && target < this.offset) || (direction === 1 && target > this.offset)) {
this.offset += speed * direction
} else {
this.canAnimate = true
clearInterval(this.animFrame)
this.offset = target
if (target < -2500) this.offset = -500
if (target > -500) this.offset = -2500
}
}, 20)
},
goToSlide(index) {
const dir = index - this.activeIdx >= 0 ? -1 : 1
const step = Math.abs(index - this.activeIdx) * 500
const jumpSpeed = Math.abs(index - this.activeIdx) === 0 ? this.velocity : Math.abs(index - this.activeIdx) * this.velocity
this.shiftSlide(step, dir, jumpSpeed)
}
}
}
</script>