Composables

usePagination

Manage pagination state with URL query parameters for seamless navigation and shareable links.

The usePagination composable provides URL-synced pagination state management. It automatically handles page and page size changes through URL query parameters, making pagination states shareable and bookmarkable. Works seamlessly with Nuxt UI's UPagination and USelect components.

Basic Usage

<script setup lang="ts">
const { page, pageSize } = usePagination()

// Fetch data with current pagination
const { data } = await useFetch('/api/users', {
  query: {
    page,
    size: pageSize,
  },
})
</script>

<template>
  <div>
    <!-- Your data display -->
    <UserList :users="data.items" />

    <!-- Pagination controls -->
    <div class="flex items-center justify-between gap-4">
      <USelect
        v-model="pageSize"
        :options="[10, 25, 50, 100]"
      />
      <UPagination
        v-model="page"
        :total="data.total"
        :page-size="pageSize"
      />
    </div>
  </div>
</template>

Options

The usePagination composable accepts an optional configuration object:

PropertyTypeDefaultDescription
defaultPagenumber1The default page number when no query parameter is present
defaultSizenumber10The default page size when no query parameter is present
routeReturnType<typeof useRoute>useRoute()The route object to read query parameters from

Return Value

Returns an object with two reactive computed properties:

  • page - Current page number (getter/setter that updates URL)
  • pageSize - Current page size (getter/setter that updates URL)

Both properties automatically:

  • Read from URL query parameters (?page=2&size=25)
  • Update URL when changed (via navigateTo)
  • Remove default values from URL for cleaner links

Examples

With Custom Defaults

const { page, pageSize } = usePagination({
  defaultPage: 1,
  defaultSize: 25, // Start with 25 items per page
})

Server-Side Pagination

<script setup lang="ts">
const { page, pageSize } = usePagination()

const { data, pending, refresh } = await useFetch('/api/products', {
  query: {
    page,
    size: pageSize,
  },
})
</script>

<template>
  <div>
    <UProgress v-if="pending" />
    
    <ProductGrid :products="data.items" />

    <div class="flex justify-between mt-6">
      <USelect
        v-model="pageSize"
        :options="[10, 25, 50]"
        label="Items per page"
      />
      <UPagination
        v-model="page"
        :total="data.total"
        :page-size="pageSize"
      />
    </div>
  </div>
</template>

With Search and Filters

<script setup lang="ts">
const route = useRoute()
const { page, pageSize } = usePagination({ route })

// Other query parameters
const search = computed(() => route.query.search as string || '')

const { data } = await useFetch('/api/posts', {
  query: {
    page,
    size: pageSize,
    search,
  },
})
</script>

<template>
  <div>
    <UInput v-model="search" placeholder="Search posts..." />
    
    <PostList :posts="data.items" />
    
    <UPagination
      v-model="page"
      :total="data.total"
      :page-size="pageSize"
    />
  </div>
</template>

Multiple Paginated Lists

When you have multiple paginated lists on the same page, pass different route instances or use different query parameter names:

<script setup lang="ts">
// For the main list
const { page: userPage, pageSize: userPageSize } = usePagination({
  defaultSize: 10,
})

// For a secondary list, you'd need custom query params
// (the composable currently uses 'page' and 'size' by default)
</script>

Behavior Details

URL Query Synchronization

The composable keeps pagination state in sync with URL query parameters:

  • On mount: Reads ?page=X&size=Y from URL
  • On change: Updates URL via navigateTo
  • Default values: Removed from URL (e.g., ?page=1 becomes /)
// URL: /products
// page = 1 (default), pageSize = 10 (default)

page.value = 2
// URL: /products?page=2

pageSize.value = 25
// URL: /products?size=25 (page reset to 1, removed from URL)

page.value = 1
// URL: /products?size=25 (page=1 removed as it's the default)

Page Size Reset

When changing page size, the page automatically resets to 1 to avoid out-of-bounds errors:

// User is on page 5 with 10 items per page
page.value = 5
pageSize.value = 10

// User changes to 50 items per page
pageSize.value = 50
// page automatically becomes 1

Integration with Backend

Your API endpoint should accept page and size query parameters:

server/api/items.get.ts
export default defineEventHandler(async (event) => {
  const query = getValidatedQuery(event, paginationSchema.parse)
  const { page = 1, size = 10 } = query

  const offset = (page - 1) * size
  const items = await db.query.items.findMany({
    limit: size,
    offset,
  })

  const total = await db.select({ count: sql`count(*)` })
    .from(items)
    .then(res => Number(res[0].count))

  return {
    items,
    total,
    page,
    size,
  }
})

Notes

The composable uses useParsedQuery internally to validate and parse URL query parameters, ensuring type safety and preventing invalid values.
Both page and pageSize are reactive computed properties with getters and setters. This allows them to work seamlessly with v-model directives on Nuxt UI components.
When the page size changes, the current page is always reset to 1. This prevents scenarios where the user might be on page 10 with 10 items per page (showing items 91-100), but then changes to 50 items per page where page 10 might not exist.
Query parameters with default values are automatically removed from the URL to keep links clean and prevent unnecessary query strings in the URL bar.

Copyright © 2026