Intro
As you can see in the VueJS docs section for SwiperJS it says:
Swiper Vue.js components are compatible only with new Vue.js version 3
And I immediately got upset because at the time this article is being written, Vue 3.0 is still in preview state, and most of the projects are still running on Vue 2.0.
As for me it feels kinda lame to migrate to Vue 3.0 only because of the swiper library, also there are other options like vue-awesome-swiper, but why would you use a wrapper library that is using old SwiperJS.
So... here is make take:
Preparation
Let's quickly bootstrap our project by running npx create-nuxt-app article-nuxt-swiper
in the terminal.
Here are all the options that i have chosen in the CLI:
Now let's move to the directory of our project by running cd article-nuxt-swiper
and add some scss by running in the terminal:
using npm:
npm install --save-dev node-sass sass-loader @nuxtjs/style-resources
using yarn:
yarn add --dev node-sass sass-loader @nuxtjs/style-resources
and let's add SwiperJS by running:
using npm:
npm install swiper
using yarn:
yarn add swiper
Then I have disabled buefy css
import in nuxt.config.js
:
// nuxt.config.js
modules: [
// https://go.nuxtjs.dev/buefy
['nuxt-buefy', { css: false }],
],
And added bulma's and buefy's scss like that:
// nuxt.config.js
css: [
'@/assets/scss/main.scss'
],
buildModules: [
// other stuff
'@nuxtjs/style-resources'
],
styleResources: {
scss: ['@/assets/scss/_variables.scss']
},
// @assets/scss/main.scss
@charset "utf-8";
@import "~bulma";
@import "~buefy/src/scss/buefy";
@import "./_swiper.scss"
// @assets/scss/_variables.scss
$fullhd-enabled: false;
@import "~bulma/sass/utilities/_all.sass";
@import "~buefy/src/scss/utils/_all.scss";
// @assets/scss/_swiper.scss
@import '~swiper/swiper.scss';
@import '~swiper/components/navigation/navigation.scss';
@import '~swiper/components/pagination/pagination.scss';
I have also slightly adjusted some other configs for better TypeScript experience:
// package.json
"lint-staged": {
"*.{js,vue}": "eslint"
},
to:
// package.json
"lint-staged": {
"*.{ts,js,vue}": "eslint"
},
in nuxt.config.js
export default {
// your other stuff
typescript: {
typeCheck: {
eslint: {
files: './**/*.{ts,js,vue}'
}
}
}
}
in tsconfig.json
{
// your other stuff
"compilerOptions": {
// your other stuff
"types": [
"@types/node",
"@nuxt/types",
"@nuxtjs/axios"
]
},
}
and in the end have installed nuxt-property-decorator by running:
using npm:
npm install nuxt-property-decorator
using yarn:
yarn add nuxt-property-decorator
Slides
Before we hop into the slider itself, let's first quickly create some markup for our slides. We will have three different types of slides and I will put them into article-nuxt-swiper/components/Slider/templates/<name_of_the_slide>.vue
I will just throw some markup at you:
Slide #1:
<template>
<div
:style="`background-image: url(${slide.url})`"
class="slide-with-big-picture"
>
<div class="slide-with-big-picture__main">
<img class="slide-with-big-picture__picture" :src="slide.thumbnailUrl">
</div>
<div class="slide-with-big-picture__description">
<p class="slide-with-big-picture__text">
{{ slide.title }}
</p>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'
import { Slide } from '../../../types/components/slides.interface'
@Component({})
export default class SlideWithBigPicture extends Vue {
@Prop({ required: true, type: Object }) readonly slide!: Slide
}
</script>
<style lang="scss">
.slide-with-big-picture {
display: flex;
position: relative;
height: 252px;
justify-content: center;
align-items: center;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: cover;
+tablet-only {
height: 240px;
}
+mobile {
height: 192px;
}
&__main {
display: flex;
position: absolute;
width: 150px;
height: 150px;
align-items: center;
justify-content: center;
background-color: #fff;
border-radius: 4px;
z-index: 3;
}
&__bg {
position: absolute;
}
&__picture {
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
}
&__description {
display: flex;
flex-direction: column;
box-sizing: border-box;
padding: 16px 20px;
width: 100%;
height: 94px;
bottom: 0;
margin-top: auto;
background: rgba(32, 42, 37, 0.6);
color: #fff;
z-index: 2;
+mobile {
height: 74px;
padding: 12px;
}
}
&__title,
&__text {
line-height: 16px;
+mobile {
line-height: 12px;
}
}
&__title {
font-size: 12px;
margin-bottom: 6px;
+mobile {
font-size: 10px;
}
}
&__text {
font-weight: 500;
font-size: 16px;
+mobile {
font-size: 12px;
}
}
}
</style>
Slide #2:
<template>
<div
class="slide-with-small-picture"
>
<img :src="slide.thumbnailUrl" class="slide-popular-retailer__picture">
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'
import { Slide } from '../../../types/components/slides.interface'
@Component({})
export default class SlidePopularRetailer extends Vue {
@Prop({ required: true, type: Object }) readonly slide!: Slide
}
</script>
<style lang="scss">
.slide-with-small-picture {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background-color: grey;
}
</style>
Slide #3:
<template>
<div
class="slide-with-text"
>
<span class="slide-with-text__name">{{ slide.title }}</span>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'
import { Slide } from '../../../types/components/slides.interface'
@Component({})
export default class SlideWithText extends Vue {
@Prop({ required: true, type: Object }) readonly slide!: Slide
}
</script>
<style lang="scss">
.slide-with-text {
display: flex;
position: relative;
height: 108px;
justify-content: center;
align-items: center;
z-index: 2;
background:yellow;
&::after {
z-index: 1;
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: linear-gradient(180deg, rgba(22, 101, 193, 0.18) 0%, rgba(22, 101, 193, 0.63) 0%, rgba(5, 34, 68, 0.9) 147.22%);
}
&__name {
color: #fff;
font-weight: bold;
font-size: 16px;
line-height: 20px;
text-align: center;
z-index: 3;
}
}
</style>
Slider
For better understanding I will break things up into four parts:
- markup
- coding
- styles
- and settings for our slider.
Markup
<template>
<div
class="slider"
:class="`slider--${type}`"
>
<div
class="swiper-button-prev"
:class="`swiper-button-prev--${type}`"
/>
<div
class="swiper-button-next"
:class="`swiper-button-next--${type}`"
/>
<div
:class="`swiper-container--${type}`"
class="swiper-container"
>
<div class="swiper-wrapper">
<div
v-for="(slide, index) in slides"
:key="index"
class="swiper-slide"
>
<component :is="getSlide" :slide="slide" />
</div>
</div>
<div class="swiper-pagination" />
</div>
</div>
</template>
As you can see there is a lot of
:class="`someCssClass--${type}`"
thing going on. Thistype
thing is a prop that will be passed to our slider component. And I'm using dynamic classes for easier further stylingThe
swiper-button-prev
,swiper-button-next
andswiper-container
classes are on the same level, but all of them are insideslider
class. That is also done for easier styling of the previous and next navigations buttons, because in get started page of SwiperJS documentation those navigation buttons are withinswiper-container
, thus making navigation buttons harder to style if you want those buttons to be outside of the slider itselfAnd the third thing that I want to talk about in our markup is the slide
<component :is="getSlide" :slide="slide" />
. Here I'm using dynamic components to determine which slide component has to be imported based on thetype
prop that we have passed to our slider component and we also pass aslide
prop to the slide with some data that will be displayed in that slide
Coding
I've made some comments in the code, other important stuff will be written below the code. If there is some frustration regarding typescript, please, leave a comment and I will try to help you in my spare time.
<script lang="ts">
// this is needed for typescript, omit if you are using javascript
import { Vue, Component, Prop } from 'nuxt-property-decorator'
// here we import SwiperJS library, you can name the way you want,
// for e.g. - SwiperInstance, SwiperCore or just Swiper
import SwiperInstance, { Navigation, Pagination, A11y } from 'swiper'
// this is needed for typescript, omit if you are using javascript
import { SwiperOptions, Swiper } from 'swiper/swiper.d'
// this is needed for typescript, omit if you are using javascript
import { Slide } from '../../types/components/slides.interface'
// Here we import our settings from a separate .ts file
// We will talk about it a bit later.
import settings from './settings'
// Here we configure out Swiper to use additional modules
SwiperInstance.use([Navigation, Pagination, A11y])
const SlideWithBigPicture = () => import('./templates/SlideWithBigPicture.vue')
const SlideWithSmallPicture = () => import('./templates/SlideWithSmallPicture.vue')
const SlideWithText = () => import('./templates/SlideWithText.vue')
@Component({
components: {
SlideWithBigPicture,
SlideWithSmallPicture,
SlideWithText
}
})
export default class Slider extends Vue {
@Prop({ required: true, type: Array }) readonly slides!: Slide[]
@Prop({ required: true, type: String }) readonly type!: string
private swiperInstance: Swiper = {} as Swiper
private settings: SwiperOptions = settings[this.type]
get getSlide () {
switch (this.type) {
case 'with-small-picture':
return 'SlideWithSmallPicture'
case 'with-text':
return 'SlideWithText'
case 'with-big-picture':
return 'SlideWithBigPicture'
default:
break
}
}
mounted () {
this.swiperInstance = new SwiperInstance(`.swiper-container--${this.type}`, this.settings)
}
}
</script>
-
As I have already mentioned I'm using dynamic components along with their async importing like that:
const SlideWithBigPicture = () => import('./templates/SlideWithBigPicture.vue') const SlideWithSmallPicture = () => import('./templates/SlideWithSmallPicture.vue') const SlideWithText = () => import('./templates/SlideWithText.vue')
And then I register them as usual in the
components
object of
VueJS:
@Component({ components: { SlideWithBigPicture, SlideWithSmallPicture, SlideWithText } })
-
Then we define two props in the slider component:
type
that will tell which slide component to load andslides
that is an array of our slides
@Prop({ required: true, type: Array }) readonly slides!: Slide[] @Prop({ required: true, type: String }) readonly type!: string
-
Then we define two properties:
swiperInstance
which will hold our SwiperJS object andsettings
which will hold settings of our slider.
private swiperInstance: Swiper = {} as Swiper private settings: SwiperOptions = settings[this.type]
Also, I want to mention that I do this:
settings[this.type]
,
I'm doing it because the settings that we import into the slider
component can be a huge object with a lot of settings for each
slide type, by accessing only one property from this object we
are cutting a lot of useless data. -
Then we have this:
get getSlide () { switch (this.type) { case 'with-small-picture': return 'SlideWithSmallPicture' case 'with-text': return 'SlideWithText' case 'with-big-picture': return 'SlideWithBigPicture' default: break } }
Our
get getSlide () {}
is a computed property inside of which there
is a switch statement that takes ourtype
prop as an argument
and returns a corresponding VueJS component. -
And finally we have this:
mounted () { this.swiperInstance = new SwiperInstance(`.swiper-container--${this.type}`, this.settings) }
Here we are passing our imported SwiperInstance into VueJS
property and with a class name of our slider as first argument
and settings for a slider as a second argument.We do everything in the mounted hook because
we need our markup to be already rendered in order for SwiperJS
to pick it up and initiate.
Styles
Screw this, I'm just throwing some scss at you:
<style lang="scss">
.slider {
position: relative;
.swiper-button-next,
.swiper-button-prev {
outline: none;
}
.swiper-container {
z-index: unset;
}
}
.slider--with-big-picture {
.swiper-button-next,
.swiper-button-prev {
@include touch {
display: none;
}
display: inline-flex;
top: -56px;
left: unset;
right: 0px;
bottom: unset;
margin: auto;
width: 32px;
height: 32px;
border: 1px solid #000;
border-radius: 50%;
outline: none;
&::after {
font-size: 10px;
color: #000;
font-weight: bold;
}
}
.swiper-button-prev {
right: 44px;
}
.swiper-pagination {
display: flex;
position: static;
justify-content: center;
margin-top: 20px;
@include mobile {
margin-top: 12px;
}
.swiper-pagination-bullet {
margin-right: 8px;
}
.swiper-pagination-bullet-active {
background-color: blue;
}
}
}
.slider--with-small-picture,
.slider--with-text {
@include tablet-only {
margin-right: -40px;
}
@include mobile {
margin-right: -16px;
}
.swiper-pagination {
display: none;
}
.swiper-button-disabled {
display: none;
}
.swiper-button-prev,
.swiper-button-next {
@include touch {
display: none;
}
height: 40px;
width: 40px;
background-color: #fff;
border-radius: 50%;
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.15);
&::after {
font-size: 14px;
color: #000;
font-weight: bold;
}
}
.swiper-button-next {
right: -20px;
}
.swiper-button-prev {
left: -20px;
}
}
</style>
Settings
So here is out settings object:
// this is needed for typescript, omit if you are using javascript
import { SwiperOptions } from 'swiper/types/swiper-options'
// : { [key: string]: SwiperOptions } is for typescript users
const settings: { [key: string]: SwiperOptions } = {
'with-small-picture': {
slidesPerView: 2.5,
slidesPerGroup: 1,
slidesOffsetAfter: 16,
spaceBetween: 8,
navigation: {
nextEl: '.swiper-button-next--with-small-picture',
prevEl: '.swiper-button-prev--with-small-picture'
},
breakpoints: {
769: {
slidesPerView: 4.5,
slidesPerGroup: 1.5,
spaceBetween: 16,
slidesOffsetAfter: 40
},
1024: {
slidesPerView: 5.5,
slidesPerGroup: 5.5,
slidesOffsetAfter: 0,
spaceBetween: 16
}
}
},
'with-text': {
slidesPerView: 1.75,
slidesPerGroup: 1,
centeredSlides: true,
centeredSlidesBounds: true,
slidesOffsetAfter: 16,
spaceBetween: 8,
navigation: {
nextEl: '.swiper-button-next--with-text',
prevEl: '.swiper-button-prev--with-text'
},
breakpoints: {
769: {
slidesPerView: 3.2,
centeredSlides: false,
centeredSlidesBounds: false,
slidesPerGroup: 1.2,
spaceBetween: 16,
slidesOffsetAfter: 40
},
1024: {
slidesPerView: 4,
slidesPerGroup: 4,
slidesOffsetAfter: 0,
spaceBetween: 16
}
}
},
'with-big-picture': {
slidesPerView: 1,
spaceBetween: 16,
pagination: {
el: '.swiper-pagination',
clickable: true
},
navigation: {
nextEl: '.swiper-button-next--with-big-picture',
prevEl: '.swiper-button-prev--with-big-picture'
},
breakpoints: {
769: {
slidesPerView: 2
},
1024: {
slidesPerView: 3,
slidesPerGroup: 3
}
}
}
}
export default settings
Our const settings = {}
is an object that holds three child objects, each of one has a name of the slide as a key property and contains properties of SwiperJS. As I already said, in Slide.vue
we do this: private settings: SwiperOptions = settings[this.type]
so we accessing only one child object of settings object.
Final
Well, that's it.
Now we only have to create a page and import our slider with different type
props.
<template>
<main class="page--main">
<div class="container">
<slider
class="page__slider"
type="with-big-picture"
:slides="slides"
/>
<slider
class="page__slider"
type="with-small-picture"
:slides="slides"
/>
<slider
type="with-text"
class="page__slider"
:slides="slides"
/>
</div>
</main>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import Slider from '../components/Slider/Slider.vue'
import { Slide } from '../types/components/slides.interface'
@Component({
components: {
Slider
},
async asyncData ({ $axios }) {
try {
const response = await $axios.$get('https://jsonplaceholder.typicode.com/photos?_start=0&_limit=10')
return {
slides: response
}
} catch (error) {
}
}
})
export default class MainPage extends Vue {
private slides: Slide[] = []
}
</script>
<style lang="scss">
.page--main {
padding: 100px 0px;
.page {
&__slider {
&:not(:last-of-type) {
margin-bottom: 40px;
}
}
}
.container {
@include touch {
padding: 0px 40px;
}
@include mobile {
padding: 0px 16px;
}
}
}
</style>
And voilà! Here we have it!
Links
GitHub repo can be found here - https://github.com/andynoir/article-nuxt-swiper
Live preview can be found here - https://andynoir.github.io/article-nuxt-swiper/
Top comments (3)
Thank you for the article, it shed some light on async dynamic component.
I think I am left with this as viable option where vue awesome swiper is abandoned and swiper only use pure esm export. Or, maybe shop for other library. I will try to make the JS version of this for an MVP.
Thanks again
nice article dude, love this so much.
on your code, i seems code like this
+mobile and +tablet-only
how it can be works? i think its great to have some shorthand on css media queries like that. thanks!
Hi, sorry for the late reply, these are buefy sass mixins, at first I wanted to use buefy for some styling, but at the end decided not to do so, just used some of those mixins.