I recently built a pagination component using the composition API. In this post I’m sharing my solution for doing client side pagination. All the code is available in this CodeSandbox. Let me know in the comment section bellow if you want me to share my solution for server side pagination too. I hope that you will find this useful. A screenshot of the final result

The composable pagination state

I identified the following state to be neccessary to track for a fully functioning pagination component:

  • Number of pages: The total number of available pages
  • Current page: The page that is currently being displayed
  • Rows per page: The number of rows to display on each page

This state is contained in a composable function called useClientSidePagination The composable function returns the state as a readonly property, preventing the calling component from directly modifying the state. Instead, the state is modified from a couple of functions/setters that is exposed/returned from the composable. These are:

  • setCurrentPage: set current page directly
  • previousPage: set current page to current page -1
  • nextPage: set current page to current page + 1

Here is what the composable function looks like:

useClientSidePagination.ts

import { computed, reactive, readonly, Ref, watch } from "vue";

interface PaginationConfig {
  rowsPerPage: number;
  arrayToPaginate: Ref<any[]>;
}

export interface PaginationContext {
  state: {
    numberOfPages: number;
    currentPage: number;
    rowsPerPage: number;
  };
  paginatedArray: Ref<any[]>;
  setCurrentPage(pageNumber: number): void;
  previousPage(): void;
  nextPage(): void;
}

export function usePagination(config: PaginationConfig) {
  const paginationState = reactive({
    numberOfPages: 0,
    currentPage: 1,
    rowsPerPage: config?.rowsPerPage || 8
  });

  const previousPage = async () => {
    if (paginationState.currentPage > 1) {
      paginationState.currentPage--;
    }
  };

  const setCurrentPage = async (pageNumber: number) => paginationState.currentPage = pageNumber;

  const nextPage = async () => {
    if (paginationState.numberOfPages > paginationState.currentPage) {
      paginationState.currentPage++;
    }
  };

  watch(config.arrayToPaginate, () => {
    paginationState.numberOfPages = Math.ceil(
      (config.arrayToPaginate.value.length || 0) / paginationState.rowsPerPage
    );
  });

  const paginatedArray = computed(() =>
    config.arrayToPaginate.value.slice(
      (paginationState.currentPage - 1) * paginationState.rowsPerPage,
      paginationState.currentPage * paginationState.rowsPerPage
    )
  );

  const paginationContext: PaginationContext = {
    setCurrentPage,
    previousPage,
    nextPage,
    state: readonly(paginationState),
    paginatedArray
  };

  return paginationContext;
}

The pagination component

Just having a nice composable function for pagination isn’t enough. You need something that the user can see and interact with for it to be useful. This is what the Vue component bellow if for.

The component requires a pagination context to be provided as a prop, it will use the state of the pagination context to render itself, and will call the neccessary setters when a user is interacting with the component.

PaginationComponent.vue

<template>
  <div
    v-if="paginationContext.state.numberOfPages > 1"
    class="pagination-container"
    aria-label="..."
  >
    <ul v-if="paginationContext.state.numberOfPages" class="pagination">
      <li
        class="page-item"
        :class="{ disabled: paginationContext.state.currentPage === 1 }"
        @click="paginationContext.previousPage"
      >
        <span class="page-link">&laquo;</span>
      </li>
      <li
        v-for="index in paginationContext.state.numberOfPages"
        :key="index"
        class="page-item"
        @click="paginationContext.setCurrentPage(index)"
      >
        <div
          class="page-link"
          :class="{
            'active-page': paginationContext.state.currentPage === index,
          }"
        >
          {{ index }}
        </div>
      </li>
      <li
        class="page-item"
        :class="{
          disabled:
            paginationContext.state.currentPage ===
              paginationContext.state.numberOfPages ||
            !paginationContext.state.numberOfPages,
        }"
        @click="paginationContext.nextPage()"
      >
        <div class="page-link">&raquo;</div>
      </li>
    </ul>
  </div>
</template>

<script lang="ts" setup>
import { defineProps } from "vue";

const props = defineProps({
  paginationContext: {
    required: true,
  },
});
</script>

<style scoped lang="scss">
.pagination-container {
  display: inline-block;
}

.pagination {
  margin: 0px;
  padding: 0px;
  display: flex;
  border: none;
  box-sizing: border-box;
  overflow: hidden;
  word-wrap: break-word;
  align-content: center;
  border-radius: 14px;
  height: 60px;
  box-shadow: rgb(0 0 0 / 0%) 0px 0px 0px 0px, rgb(0 0 0 / 0%) 0px 0px 0px 0px,
    rgb(0 0 0 / 12%) 0px 1px 1px 0px, rgb(60 66 87 / 16%) 0px 0px 0px 1px,
    rgb(0 0 0 / 0%) 0px 0px 0px 0px, rgb(0 0 0 / 0%) 0px 0px 0px 0px,
    rgb(60 66 87 / 8%) 0px 2px 5px 0px;
}

.page-item {
  flex: 1;
  display: flex;
  border: none;
  justify-content: center;
  cursor: pointer;
  margin-bottom: 0px;
  height: 60px;
}

.page-link {
  color: #666B85;
  float: left;
  border: none;
  padding: 0px 25px;
  font-size: 14px;
  font-weight: 800;
  line-height: 60px;
  text-decoration: none;
  &:hover {
    color: #333333;
    background-color: #e9e9e9;
    border: none;
  }
}

.active-page {
  background-color: #60d394 !important;
  border: none;
  color: white !important;
  &:hover {
    border: none;
  }
}

.disabled {
  .page-link {
    background-color: #f9fafb;
  }
  cursor: not-allowed;
}
</style>

That’s it. Enjoy! If you have any questions, just ask in the comment section!