Reusable slider component with SwiperJS and NuxtJS
Tags: vue, nuxt, typescript, swiper
Published: 2020-12-02 17:08:24

# Intro

As you can see in the VueJS docs section for [SwiperJS](https://swiperjs.com/vue/) 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](https://github.com/surmon-china/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:
![Nuxt CLI options](https://dev-to-uploads.s3.amazonaws.com/i/d3gwqp46tq7prql2auyr.png)

Now let's move to the directory of our project by running `cd article-nuxt-swiper` and add some [scss](https://sass-lang.com/) by running in the terminal:

using npm:
```zsh
npm install --save-dev node-sass sass-loader @nuxtjs/style-resources   
```

using yarn:
```zsh
yarn add --dev node-sass sass-loader @nuxtjs/style-resources   
``` 

and let's add SwiperJS by running:

using npm:
```zsh
npm install swiper
```

using yarn:
```zsh
yarn add swiper
``` 

Then I have disabled buefy `css` import in `nuxt.config.js`:

```javascript
// nuxt.config.js
  modules: [
    // https://go.nuxtjs.dev/buefy
    ['nuxt-buefy', { css: false }],
  ],
```

And added bulma's and buefy's scss like that: 

```javascript
// nuxt.config.js
  css: [
    '@/assets/scss/main.scss'
  ],

  buildModules: [
    // other stuff
    '@nuxtjs/style-resources'
  ],

  styleResources: {
    scss: ['@/assets/scss/_variables.scss']
  },
```

```scss
// @assets/scss/main.scss
@charset "utf-8";

@import "~bulma";
@import "~buefy/src/scss/buefy";

@import "./_swiper.scss"
```

```scss
// @assets/scss/_variables.scss
$fullhd-enabled: false;

@import "~bulma/sass/utilities/_all.sass";
@import "~buefy/src/scss/utils/_all.scss";
```

```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:

```json
// package.json
  "lint-staged": {
    "*.{js,vue}": "eslint"
  },
```

to:
```json
// package.json
  "lint-staged": {
    "*.{ts,js,vue}": "eslint"
  },
```

in `nuxt.config.js`

```javascript
export default {
// your other stuff 
typescript: {
    typeCheck: {
      eslint: {
        files: './**/*.{ts,js,vue}'
      }
    }
  }
}
```

in `tsconfig.json`

```json
{
  // your other stuff
  "compilerOptions": {
    // your other stuff
    "types": [
      "@types/node",
      "@nuxt/types",
      "@nuxtjs/axios"
    ]
  },
}
```

and in the end have installed [nuxt-property-decorator](https://github.com/nuxt-community/nuxt-property-decorator) by running: 

using npm:
```zsh
npm install nuxt-property-decorator
```

using yarn:
```zsh
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:

```vue
<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:

```vue
<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:

```vue
<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

```html
<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>
```
1. As you can see there is a lot of ``:class="`someCssClass--${type}`"`` thing going on. This `type` thing is a [prop](https://vuejs.org/v2/guide/components-props.html) that will be passed to our slider component. And I'm using [dynamic classes](https://vuejs.org/v2/guide/class-and-style.html) for easier further styling

2. The `swiper-button-prev`, `swiper-button-next` and `swiper-container` classes are on the same level, but all of them are inside `slider` class. That is also done for easier styling of the previous and next navigations buttons, because in [get started](https://swiperjs.com/get-started/) page of SwiperJS documentation those navigation buttons are within `swiper-container`, thus making navigation buttons harder to style if you want those buttons to be outside of the slider itself

3. And 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](https://vuejs.org/v2/guide/components-dynamic-async.html) to determine which slide component has to be imported based on the `type` prop that we have passed to our slider component and we also pass a `slide` 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.

```vue
<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>
```
1. As I have already mentioned I'm using [dynamic components](https://vuejs.org/v2/guide/components-dynamic-async.html) along with their [async importing](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components) like that:

    ```javascript

        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:

    ```typescript

        @Component({
          components: {
            SlideWithBigPicture,
            SlideWithSmallPicture,
            SlideWithText
          }
        })

    ```


2. Then we define two props in the slider component: `type` that will tell which slide component to load and `slides` that is an array of our slides

    ```typescript

        @Prop({ required: true, type: Array }) readonly slides!: Slide[]
        @Prop({ required: true, type: String }) readonly type!: string

    ```

3. Then we define two properties: `swiperInstance` which will hold our SwiperJS object and `settings` which will hold settings of our slider.

    ```typescript

        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.

4. Then we have this:

    ```typescript

      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]
    (https://vuejs.org/v2/guide/computed.html) inside of which there 
    is a switch statement that takes our `type` prop as an argument 
    and returns a corresponding VueJS component.

5. And finally we have this:

    ```typescript

        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] 
    (https://nuxtjs.org/docs/2.x/concepts/nuxt-lifecycle/) 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:

```javascript
<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:

```typescript
// 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](https://swiperjs.com/api/#methods) 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.


```vue
<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/