How to build pagination in Vue

Large datasets need pagination to improve performance and user experience by loading and displaying data in manageable chunks. With over 12 years of Vue.js development experience since 2014 and as the creator of CoreUI, I’ve implemented pagination in countless data tables and search results. A Vue pagination component requires reactive state for the current page, computed properties for page ranges, and methods for navigation. This approach creates a flexible, reusable pagination system without external dependencies.

Build a pagination component using computed properties for page calculations and event emission for page changes.

<template>
  <nav class='pagination' v-if='totalPages > 1'>
    <ul class='pagination-list'>
      <li>
        <button
          class='pagination-button'
          :disabled='currentPage === 1'
          @click='changePage(currentPage - 1)'
        >
          Previous
        </button>
      </li>

      <li v-if='showFirstPage'>
        <button
          class='pagination-button'
          :class='{ active: currentPage === 1 }'
          @click='changePage(1)'
        >
          1
        </button>
      </li>

      <li v-if='showFirstEllipsis'>
        <span class='pagination-ellipsis'>...</span>
      </li>

      <li v-for='page in visiblePages' :key='page'>
        <button
          class='pagination-button'
          :class='{ active: currentPage === page }'
          @click='changePage(page)'
        >
          {{ page }}
        </button>
      </li>

      <li v-if='showLastEllipsis'>
        <span class='pagination-ellipsis'>...</span>
      </li>

      <li v-if='showLastPage'>
        <button
          class='pagination-button'
          :class='{ active: currentPage === totalPages }'
          @click='changePage(totalPages)'
        >
          {{ totalPages }}
        </button>
      </li>

      <li>
        <button
          class='pagination-button'
          :disabled='currentPage === totalPages'
          @click='changePage(currentPage + 1)'
        >
          Next
        </button>
      </li>
    </ul>

    <div class='pagination-info'>
      Page {{ currentPage }} of {{ totalPages }}
    </div>
  </nav>
</template>

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

const props = defineProps({
  currentPage: {
    type: Number,
    required: true
  },
  totalItems: {
    type: Number,
    required: true
  },
  itemsPerPage: {
    type: Number,
    default: 10
  },
  maxVisiblePages: {
    type: Number,
    default: 5
  }
})

const emit = defineEmits(['page-change'])

const totalPages = computed(() => {
  return Math.ceil(props.totalItems / props.itemsPerPage)
})

const visiblePages = computed(() => {
  const pages = []
  const half = Math.floor(props.maxVisiblePages / 2)
  let start = Math.max(2, props.currentPage - half)
  let end = Math.min(totalPages.value - 1, props.currentPage + half)

  // Adjust if at start or end
  if (props.currentPage <= half) {
    end = Math.min(totalPages.value - 1, props.maxVisiblePages)
  }
  if (props.currentPage >= totalPages.value - half) {
    start = Math.max(2, totalPages.value - props.maxVisiblePages + 1)
  }

  for (let i = start; i <= end; i++) {
    pages.push(i)
  }

  return pages
})

const showFirstPage = computed(() => {
  return totalPages.value > 1
})

const showLastPage = computed(() => {
  return totalPages.value > 1
})

const showFirstEllipsis = computed(() => {
  return visiblePages.value.length > 0 && visiblePages.value[0] > 2
})

const showLastEllipsis = computed(() => {
  return visiblePages.value.length > 0 && visiblePages.value[visiblePages.value.length - 1] < totalPages.value - 1
})

const changePage = (page) => {
  if (page >= 1 && page <= totalPages.value) {
    emit('page-change', page)
  }
}
</script>

<style scoped>
.pagination {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  margin: 20px 0;
}

.pagination-list {
  display: flex;
  list-style: none;
  gap: 5px;
  margin: 0;
  padding: 0;
}

.pagination-button {
  padding: 8px 12px;
  border: 1px solid #ddd;
  background: white;
  cursor: pointer;
  border-radius: 4px;
  transition: all 0.2s;
}

.pagination-button:hover:not(:disabled) {
  background: #f0f0f0;
  border-color: #999;
}

.pagination-button.active {
  background: #007bff;
  color: white;
  border-color: #007bff;
}

.pagination-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.pagination-ellipsis {
  padding: 8px 12px;
  color: #999;
}

.pagination-info {
  color: #666;
  font-size: 14px;
}
</style>

Use it in your component:

<template>
  <div>
    <div v-for='item in paginatedItems' :key='item.id'>
      {{ item.name }}
    </div>

    <Pagination
      :current-page='currentPage'
      :total-items='items.length'
      :items-per-page='10'
      @page-change='currentPage = $event'
    />
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import Pagination from './Pagination.vue'

const items = ref(Array.from({ length: 100 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` })))
const currentPage = ref(1)

const paginatedItems = computed(() => {
  const start = (currentPage.value - 1) * 10
  const end = start + 10
  return items.value.slice(start, end)
})
</script>

Best Practice Note

Calculate totalPages from totalItems and itemsPerPage to ensure accurate page count. Use computed properties for visible page numbers to handle edge cases when near start or end. Add keyboard navigation (Arrow keys, Home, End) for accessibility. For server-side pagination, emit the page change event and let parent components handle data fetching. This is the pagination pattern we use in CoreUI for Vue—smart page number display with ellipsis, disabled state handling, and flexible configuration for enterprise data tables with thousands of rows.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author