Writing a pagination component in Vue 3 - with and without slots

Writing pagination from scratch can be tricky at times.

I was recently tasked with writing a pagination component to organize a variety of items on our website. In the process, I found that using slots can be helpful in organizing multiple lists across a website, depending on what you’re trying to do.

Simply put, if you are just writing pagination for a single list in your application, just letting the pagination component handle all the logic works fine.

If you want to reuse the pagination with slots, you need to have the parent component handle a bit of the logic.

Below, you'll see some examples of when and when not to implement slots for pagination.

Without slots

This example uses a card component, but you can replace that with whatever component or item you want rendered within the pagination. This is the template I used in my Pagination.Vue component

<template>
    <div>
        <div class="grid">
            <Card
                v-for="(item, index) in displayedItems"
                :key="`${item.id}--${index}`"
                :item="item"
            />
        </div>
        <div>
            <button
                type="button"
                :class="[
                    'button--link button--large',
                    { isActive: page === currentPage }
                ]"
                v-for="page in pages"
                :key="page"
                @click="changePage(page)"
            >
                {{ page }}
            </button>
        </div>
    </div>
</template>
Pagination.vue markup

Note the v-for is going through a list called displayedItems – which is a computed property. The script is as follows for Pagination.Vue:

import { ref, computed, watch } from 'vue';

export default {
    props: {
        items: {
            type: Array,
            required: true
        },
        itemsPerPage: {
            type: Number,
            default: 10
        },
    },
    setup(props) {
        const currentPage = ref(1);

        const pageCount = computed(() =>
            Math.ceil(props.items.length / props.itemsPerPage)
        );

        const pages = computed(() => {
     		return Array.from({ length: pageCount.value }, (_, i) => i + 1);
        });

        const displayedItems = computed(() => {
            const startIndex = (currentPage.value - 1) * props.itemsPerPage;
            const endIndex = startIndex + props.itemsPerPage;

            return props.items.slice(startIndex, endIndex);
        });

        function changePage(pageNumber) {
            currentPage.value = pageNumber;
        }

        watch(currentPage, () => {
            //If you want the window to scroll up on page change
            //window.scrollTo({
            //     top: 0,
            //     behavior: 'smooth'
            // });
        });

        return {
            currentPage,
            pageCount,
            pages,
            displayedItems,
            changePage
        };
    }
};
Pagination.vue script

To call it from the parent, simply send the list and itemsPerPage as props, and it will handle the rest.

<Pagination
 :items="items"
 itemsPerPage=6
></Pagination>

Add styles in the pagination component as needed!

With slots

To make use of slots and have the component be reusable across the entire application, there needs to be a bit more logic handled by the parent component wherever it is used.

Move the variables of currentPage and itemsPerPage into the parent, as well as the computed property displayedItems.

Somewhere in the setup of your parent component, you should have this code:

setup(props) {
        const currentPage = ref(1);
        const itemsPerPage = ref(6);

        const displayedItems = computed(() => {
            const startIndex = (currentPage.value - 1) * itemsPerPage.value;
            const endIndex = startIndex + itemsPerPage.value;

            return props.courses.slice(startIndex, endIndex);
        });

		//pass this function to pagination component
        function changePage(pageNumber: number) {
            currentPage.value = pageNumber;
        }
        return {
            currentPage,
            displayedItems,
            itemsPerPage,
            changePage
        };
    }
Parent component rendering Pagination.vue

Now, the Pagination component will look like this,  with a slot set up to render whatever list you want. Note the difference in props – these will be sent by the parent.

<template>
    <div>
        <slot></slot>
        <div>
            <button
                type="button"
                :class="[
                    'button--link button--large',
                    { isActive: page === currentPage }
                ]"
                v-for="page in pages"
                :key="page"
                @click="$emit('changePage', page)"
            >
                {{ page }}
            </button>
        </div>
    </div>
</template>

<script>
import { computed } from 'vue';

export default {
    //note this emit back to parent
    emits: ['changePage'],
    props: {
        //changed to just get the length of the list
        itemCount: {
            type: Number,
            required: true
        },
        itemsPerPage: {
            type: Number,
            default: 10
        },
        currentPage: {
            type: Number,
            required: true
        }
    },
    setup(props) {
        const pageCount = computed(() =>
            Math.ceil(props.noOfItems / props.itemsPerPage)
        );

        const pages = computed(() => {
        	return Array.from({ length: pageCount.value }, (_, i) => i + 1);
        });

        return {
            pageCount,
            pages
        };
    }
};
</script>
Pagination.vue

You'll see that we will now emit the change page event to the parent, which will handle the logic.

The rendering from the parent now looks like this

 <Pagination
        :itemCount="items.length"
        :itemsPerPage="itemsPerPage"
        :currentPage="currentPage"
        @changePage="changePage"
    >
     <div class="grid">
         <Card
             v-for="(item, index) in displayedItems"
             :key="`${item.id}--${index}`"
             :course="item"
             />
     </div>
</Pagination>
Parent component

Slots can be useful depending on what you’re aiming to do with your page or application — but don’t be afraid to get creative with your pages and see what works. Happy coding!