Components

Pagination

Split a long list into pages, with page numbers that collapse to fit.

UiPagination renders a <nav> of page buttons. Bind the current page with v-model:page (pages start at 1) and give it either the item count and page size, or the page count. The current page gets aria-current="page".

Add a pagination

<script setup lang="ts">
import { ref } from 'vue'

const page = ref(1)
</script>

<template>
  <UiPagination v-model:page="page" :total="248" :page-size="20" />
</template>

When the page count drops below the current page, for example after a filter, the pagination moves to the last page and emits update:page.

Page on the server

Pass totalPages when the server sends a page count, and fetch the page whenever it changes:

<script setup lang="ts">
const page = ref(1)
const { data } = await useFetch('/api/invoices', { query: { page } })
</script>

<template>
  <UiDatatable :columns="columns" :items="data?.items ?? []" />
  <UiPagination
    v-model:page="page"
    :total-pages="data?.pageCount ?? 1"
    class="d-flex justify-content-end mt-3"
  />
</template>

To page rows that are already loaded, use the datatable's pagination prop instead.

Collapse long ranges

The first and last pages always show, with siblingCount pages on each side of the current one (default 1). The pages in between collapse into an ellipsis. An ellipsis never hides a single page, and the number of buttons stays the same from page to page, so the arrows don't move under the pointer.

<template>
  <!-- 1 … 8 9 10 11 12 … 20 -->
  <UiPagination v-model:page="page" :total-pages="20" :sibling-count="2" />
</template>

showEdges adds first-page and last-page buttons next to the arrows.

Compact layout

On screens narrower than 600px, the page numbers give way to the position text: ‹ Page 3 of 12 ›. Set compact to use that layout at every width, for example in a narrow card:

<template>
  <UiPagination v-model:page="page" :total-pages="12" compact show-edges />
</template>

Translate the labels

labels takes any of first, previous, next, last (the arrow buttons' accessible names) and status (the position text). To translate every pagination at once, set them app-wide; see Localization. In status, {page} and {total} are replaced with numbers, so the words can come in any order:

<template>
  <UiPagination
    v-model:page="page"
    :total-pages="8"
    aria-label="Paginación"
    :labels="{ previous: 'Página anterior', next: 'Página siguiente', status: 'Página {page} de {total}' }"
  />
</template>

Accessibility

  • At the first or last page, the arrows that can't move are marked aria-disabled instead of disabled, so keyboard focus stays on the arrow you just pressed.
  • Moving to another page announces the position text through a polite live region.
  • Give each pagination on a page its own ariaLabel.

Props

PropTypeDefaultDescription
pagenumber1Current page, starting at 1. Bind with v-model:page.
totalnumber0Number of items. With pageSize, sets the page count.
pageSizenumber10Items per page.
totalPagesnumber | nullnullPage count. Takes precedence over total and pageSize.
siblingCountnumber1Pages shown on each side of the current page.
showEdgesbooleanfalseAdd first-page and last-page buttons.
compactbooleanfalseShow the position text instead of page numbers at every width.
size'sm' | 'md' | 'lg' | null'sm'Button size.
disabledbooleanfalseDisable every button.
ariaLabelstring'Pagination'<nav> landmark name.
labelsPartial<IPaginationLabels> | nullnullText for the arrows and the position text.
customClassClassValue | nullnullClasses for the <nav>.

Labels (IPaginationLabels): first ('First page'), previous ('Previous page'), next ('Next page'), last ('Last page'), status ('Page {page} of {total}'). Emits: update:page(page).

CSS variables

Set these on :root, or on any wrapper to restyle only the paginations inside it. The page buttons are .btn elements, so their size and hover come from the button styles.

:root {
  --cffy-pagination-radius: var(--cffy-radius-sm);
  --cffy-pagination-current-bg-color: var(--cffy-primary-container);
  --cffy-pagination-current-color: var(--cffy-on-primary-container);
}
VariableDefaultControls
--cffy-pagination-gap--cffy-space-4Space between buttons
--cffy-pagination-radius--cffy-radius-fullButton corners
--cffy-pagination-current-bg-color / --cffy-pagination-current-color--cffy-primary-a10 / --cffy-on-primaryCurrent page
--cffy-pagination-status-color--cffy-on-backgroundPosition text in the compact layout
Copyright © 2026