Skip to main content
This guide shows developers how to add pagination to any data table in the DeployStack frontend.

Pagination Style

The pagination follows the shadcn-vue DataTable pattern: Without selection (default):
                                    [Rows per page ▼]  Page 1 of 7  [<<] [<] [>] [>>]
With selection enabled:
0 of 68 row(s) selected.           [Rows per page ▼]  Page 1 of 7  [<<] [<] [>] [>>]
  • Selection info (left, optional) - “X of Y row(s) selected.” Only shown when selection props are provided
  • Rows per page selector (right) - Dropdown with options: 10, 20, 30, 40, 50 (hidden on mobile)
  • Page indicator (right) - “Page X of Y”
  • Navigation buttons (right) - First, Previous, Next, Last (First/Last hidden on mobile)

Quick Implementation

1. Service Layer

Add pagination support to your service:
// services/yourService.ts
export interface PaginationParams {
  limit?: number
  offset?: number
}

export interface PaginationMeta {
  total: number
  limit: number
  offset: number
  has_more: boolean
}

export interface PaginatedResponse<T> {
  items: T[]
  pagination: PaginationMeta
}

static async getItemsPaginated(
  filters?: ItemFilters,
  pagination?: PaginationParams
): Promise<PaginatedResponse<Item>> {
  const url = new URL(`${this.baseUrl}/api/items`)

  // Add filters and pagination params
  if (filters) {
    Object.entries(filters).forEach(([key, value]) => {
      if (value !== undefined) url.searchParams.append(key, String(value))
    })
  }

  if (pagination) {
    if (pagination.limit) url.searchParams.append('limit', String(pagination.limit))
    if (pagination.offset) url.searchParams.append('offset', String(pagination.offset))
  }

  const response = await fetch(url.toString(), {
    method: 'GET',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' }
  })

  const data = await response.json()

  return {
    items: data.data.items,
    pagination: data.data.pagination
  }
}

2. Component Implementation

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import PaginationControls from '@/components/ui/pagination/PaginationControls.vue'
import { YourService, type PaginationMeta } from '@/services/yourService'

// Pagination state
const currentPage = ref(1)
const pageSize = ref(20)
const totalItems = ref(0)
const items = ref([])
const isLoading = ref(false)

// Fetch data with pagination
async function fetchItems() {
  isLoading.value = true
  try {
    const offset = (currentPage.value - 1) * pageSize.value
    const response = await YourService.getItemsPaginated(
      filters.value,
      { limit: pageSize.value, offset }
    )

    items.value = response.items
    totalItems.value = response.pagination.total
  } finally {
    isLoading.value = false
  }
}

// Event handlers
async function handlePageChange(page: number) {
  currentPage.value = page
  await fetchItems()
}

async function handlePageSizeChange(newPageSize: number) {
  pageSize.value = newPageSize
  currentPage.value = 1
  await fetchItems()
}

onMounted(() => fetchItems())
</script>

<template>
  <div class="space-y-4">
    <!-- Your data table -->
    <YourTableComponent :items="items" />

    <!-- Pagination controls -->
    <PaginationControls
      v-if="totalItems > 0"
      :current-page="currentPage"
      :page-size="pageSize"
      :total-items="totalItems"
      :is-loading="isLoading"
      @page-change="handlePageChange"
      @page-size-change="handlePageSizeChange"
    />
  </div>
</template>

3. Add Translations

Add to your i18n file (e.g., i18n/locales/en/yourFeature.ts):
pagination: {
  rowsPerPage: 'Rows per page',
  pageInfo: 'Page {current} of {total}',
  firstPage: 'Go to first page',
  previousPage: 'Go to previous page',
  nextPage: 'Go to next page',
  lastPage: 'Go to last page',
  rowsSelected: '{selected} of {total} row(s) selected.'  // Only needed if using selection
}

PaginationControls Component

Props

PropTypeRequiredDefaultDescription
currentPagenumberYes-Current page number (1-based)
pageSizenumberYes-Items per page
totalItemsnumberYes-Total number of items
isLoadingbooleanNofalseLoading state (disables navigation)
pageSizeOptionsnumber[]No[10, 20, 30, 40, 50]Available page sizes
selectedCountnumberNo-Number of selected rows (enables selection display)
totalRowsnumberNo-Total rows for selection display

Events

  • @page-change(page: number) - Emitted when page changes
  • @page-size-change(pageSize: number) - Emitted when page size changes

With Row Selection

When your table has row selection enabled, pass the selectedCount and totalRows props to show the selection info on the left:
<PaginationControls
  :current-page="currentPage"
  :page-size="pageSize"
  :total-items="totalItems"
  :selected-count="selectedRows.length"
  :total-rows="items.length"
  @page-change="handlePageChange"
  @page-size-change="handlePageSizeChange"
/>

shadcn-vue Components Used

The PaginationControls component uses these shadcn-vue components:
  • Button - For navigation buttons (icon-only, outline variant, size-8)
  • Label - For “Rows per page” label
  • Select, SelectContent, SelectItem, SelectTrigger, SelectValue - For page size selector (w-20)
  • Lucide icons: ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight

Responsive Behavior

The pagination is responsive:
ElementDesktopMobile
Selection info (if enabled)VisibleVisible
Rows per page selectorVisibleHidden
Page infoVisibleVisible
First page buttonVisibleHidden
Previous page buttonVisibleVisible
Next page buttonVisibleVisible
Last page buttonVisibleHidden
On mobile, the navigation buttons use ml-auto to push them to the right edge.

Backend Requirements

Your backend API must support these query parameters:
  • limit - Number of items per page (1-100)
  • offset - Number of items to skip
And return this response format:
{
  "success": true,
  "data": {
    "items": [...],
    "pagination": {
      "total": 150,
      "limit": 20,
      "offset": 40,
      "has_more": true
    }
  }
}