Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.deploystack.io/llms.txt

Use this file to discover all available pages before exploring further.

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
    }
  }
}