GrPagination
Splits a long list into pages and manages navigation between them.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the list does not fit on the screen — page numbers with the middle truncated;
- the user chooses the page size —
showPageSizewith the set frompageSizes; - the volume has to be known —
showTotalprints “41–60 of 137” next to the navigation; - there is little room —
compactreplaces the numbers with a “current / total” indicator; - there are many pages —
showJumpergives a jump by number instead of stepping through.
When to take something else
| Need | Take |
|---|---|
| The list is loaded by scrolling | GrList with virtual and source |
| There are few rows and they all fit | GrTable |
| Sections are switched rather than pages of data | GrTabs |
| The course of a long operation has to be shown | GrProgressBar |
page, pageSize and total arrive from the outside: the component only asks for them to be
changed. It knows nothing about the data and cannot know — the application loads it.
A controlled component
page, pageSize and total arrive from the outside, and the component only asks for them to
be changed:
<GrPagination
v-model:page="page"
v-model:page-size="pageSize"
:total="total"
/>
Precisely v-model:page and not v-model: the page is a named model, and the component has no
modelValue prop. A miss is visible as a modelvalue attribute on the root <div> — an
undeclared prop travels there through fallthrough; in dev mode the component additionally prints
a warning.
The number of pages is computed from total and pageSize. The numbers are truncated by the
boundary/sibling algorithm: the first and the last pages are always visible plus siblingCount
neighbours around the current one; boundaryCount sets how many of the edge ones to show. A
single skipped number is drawn as a number rather than as an ellipsis — the row does not jump.
A non-numeric input is replaced by the default: a page that did not arrive gives the first
page, total gives zero and pageSize gives one, and every case is explained with a warning in
dev mode. Without that an undefined would spread across the numbers and the status as NaN,
while the markup would stay plausible to the eye.
A page out of range is rendered clamped to [1, pageCount]: until the parent has pulled the
value up, there is an active button all the same. When the number of pages has decreased
(total fell or pageSize grew), the component additionally emits update:page with the last
available page — if page lives in the URL or in a store, that navigation will happen by
itself.
What to show
| Prop | What it adds |
|---|---|
showPageSize | a select for the page size; off by default — basic pagination is numbers only |
showTotal | “41–60 of 137” to the left of the navigation |
showJumper | a “go to page” field: Enter or the loss of focus applies the number, and one out of range is clamped |
compact | a “current / total” indicator instead of the numbers — for mobile and for table toolbars |
The range as a whole is replaced by the #total slot — it receives from, to and total:
<GrPagination :total="total" show-total>
<template #total="{ from, to, total }">
Orders {{ from }}–{{ to }} of {{ total }}
</template>
</GrPagination>
disabled dims everything at once: the numbers, the buttons, the select and the jump field.
Accessibility
The root is a role="navigation" with a name from the locale (gr.pagination.label). If there
are two paginations on the page (above and below a table), set ariaLabel — otherwise there will
be two identical landmarks in the overview of a screen reader.
The numbers lie in a list (<ul role="list">), so a screen reader reports their number; the
ellipses are excluded from it (aria-hidden). The current page is marked aria-current="page".
A change of page is announced by a live area: in the compact mode it is carried by the visible
indicator, and in the ordinary one by a hidden “Page N of M” string (gr.pagination.status).
The sizes
size (xs…lg) is read from GrConfigProvider (componentDefaults.GrPagination.size). The
navigation buttons take their size from the GrButton scale rather than from one of their own:
they are obliged to stand in one row with the numbers.
Playground 13
Loading…
<GrPagination />Install
npm i @feugene/granularityImport
import { GrPagination } from '@feugene/granularity/components/GrPagination'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
disabled | boolean | undefined | false | Dims the whole pagination: the numbers, the buttons, the page-size select and the jump field. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | The name of the navigation landmark. By default — `gr.pagination.label`; it is worth setting where there are several paginations on the page and they have to be told apart. |
compact | boolean | undefined | false | The compact variant: instead of numbered pages a "current / total" indicator is shown — convenient in tight places (mobile, table toolbars). |
pageSizes | number[] | undefined | [10, 20, 50] | — |
siblingCount | number | undefined | 1 | How many neighbouring pages to show around the current one. `1` by default. |
boundaryCount | number | undefined | 1 | How many edge pages to always show at each edge. `1` by default. |
showJumper | boolean | undefined | false | Show a "go to page" field with a quick jump by the entered number. |
showPageSize | boolean | undefined | false | Show the page-size select. |
showTotal | boolean | undefined | false | Show the range of the items displayed — "1–20 of 137". The `#total` slot is stronger. |
jumperLabel | string | undefined | undefined | The i18n label before the jump field. By default — `gr.pagination.jumpTo`. |
pagerequired | number | — | — |
pageSizerequired | number | — | — |
totalrequired | number | — | — |
Slots
| Slot | Type | Description |
|---|---|---|
total | { from: number; to: number; total: number; } | The range of the items displayed as a whole — instead of the string from the locale. |
Events
| Event | Type | Description |
|---|---|---|
update:page | [value: number] | — |
update:pageSize | [value: number] | — |
Examples 5
Basic paging feedback loop
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrPagination } from '@feugene/granularity'
const total = ref(137)
const page = ref(3)
const pageSize = ref(10)
</script>
<template>
<div class="grid gap-3">
<GrPagination v-model:page="page" v-model:page-size="pageSize" :total="total" show-total />
<div class="flex flex-wrap gap-2">
<GrBadge>
Page {{ page }}
</GrBadge>
<GrBadge>
Page size {{ pageSize }}
</GrBadge>
</div>
</div>
</template>Page-size changes with page clamping
<script setup lang="ts">
import { computed, ref } from 'vue'
import { GrBadge, GrButton, GrPagination } from '@feugene/granularity'
const total = ref(58)
const page = ref(5)
const pageSize = ref(12)
const pageSizes = [6, 12, 24]
const pageCount = computed(() => {
return Math.max(1, Math.ceil(total.value / pageSize.value))
})
function clampPage() {
page.value = Math.min(page.value, pageCount.value)
}
function setTotal(nextTotal: number) {
total.value = nextTotal
clampPage()
}
</script>
<template>
<div class="grid gap-4">
<div class="flex flex-wrap gap-2">
<GrButton size="sm" :variant="total === 58 ? 'primary' : 'outline'" @click="setTotal(58)">
58 items
</GrButton>
<GrButton size="sm" :variant="total === 23 ? 'primary' : 'outline'" @click="setTotal(23)">
23 items
</GrButton>
<GrButton size="sm" :variant="total === 8 ? 'primary' : 'outline'" @click="setTotal(8)">
8 items
</GrButton>
</div>
<GrPagination
v-model:page="page"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
show-page-size
:total="total"
@update:page-size="clampPage"
/>
<div class="flex flex-wrap gap-2">
<GrBadge>
Total {{ total }}
</GrBadge>
<GrBadge>
Last available page {{ pageCount }}
</GrBadge>
<GrBadge>
Active page {{ page }}
</GrBadge>
</div>
</div>
</template>Composition with GrDataTable
Actions | |||
|---|---|---|---|
| Customer 1 | Scale | attention | |
| Customer 2 | Starter | healthy | |
| Customer 3 | Scale | healthy | |
| Customer 4 | Starter | attention | |
| Customer 5 | Scale | healthy |
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
GrBadge,
GrButton,
GrDataTable,
GrPagination,
} from '@feugene/granularity'
const page = ref(1)
const pageSize = ref(5)
const pageSizes = [5, 10, 20]
const lastAction = ref('No row action yet')
const rows = Array.from({ length: 18 }, (_, index) => ({
id: index + 1,
customer: `Customer ${index + 1}`,
plan: index % 2 === 0 ? 'Scale' : 'Starter',
status: index % 3 === 0 ? 'attention' : 'healthy',
}))
const columns = [
{ key: 'customer', label: 'Customer', sortable: true },
{ key: 'plan', label: 'Plan', sortable: true },
{ key: 'status', label: 'Status', sortable: true },
{ key: 'actions', label: 'Actions', align: 'right' as const },
]
const pagedRows = computed(() => {
const start = (page.value - 1) * pageSize.value
return rows.slice(start, start + pageSize.value)
})
const pageCount = computed(() => Math.max(1, Math.ceil(rows.length / pageSize.value)))
function clampPage() {
page.value = Math.min(page.value, pageCount.value)
}
</script>
<template>
<div class="grid gap-4">
<GrDataTable :rows="pagedRows" :columns="columns" row-key="id">
<template #cell-status="{ row }">
<GrBadge :tone="row.status === 'healthy' ? 'success' : 'warning'">
{{ row.status }}
</GrBadge>
</template>
<template #cell-actions="{ row }">
<div class="flex justify-end">
<GrButton size="sm" variant="ghost" @click="lastAction = `Opened ${row.customer}`">
Open
</GrButton>
</div>
</template>
</GrDataTable>
<GrPagination
v-model:page="page"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
show-page-size
:total="rows.length"
@update:page-size="clampPage"
/>
<GrBadge>
{{ lastAction }}
</GrBadge>
</div>
</template>Compact variant and page jumper
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrDivider, GrPagination } from '@feugene/granularity'
const total = ref(482)
const page = ref(7)
const pageSize = ref(20)
</script>
<template>
<div class="grid gap-4">
<GrDivider label="compact" align="start" />
<GrPagination v-model:page="page" v-model:page-size="pageSize" :total="total" compact />
<GrDivider label="show-jumper" align="start" />
<GrPagination v-model:page="page" v-model:page-size="pageSize" :total="total" show-jumper />
<GrDivider label="compact + show-jumper" align="start" />
<GrPagination v-model:page="page" v-model:page-size="pageSize" :total="total" compact show-jumper />
<GrDivider />
<div class="flex flex-wrap gap-2">
<GrBadge>Page {{ page }}</GrBadge>
<GrBadge>Page size {{ pageSize }}</GrBadge>
<GrBadge>Total {{ total }}</GrBadge>
</div>
</div>
</template>Sizes
<script setup lang="ts">
import { ref } from 'vue'
import { GrPagination } from '@feugene/granularity'
const sizes = ['xs', 'sm', 'md', 'lg'] as const
const page = ref(4)
const pageSize = ref(20)
</script>
<template>
<div class="grid gap-4">
<div v-for="size in sizes" :key="size" class="grid gap-2">
<div class="text-xs font-semibold text-[var(--gr-muted-fg)]">
size="{{ size }}"
</div>
<GrPagination
v-model:page="page"
v-model:page-size="pageSize"
:total="240"
:size="size"
/>
</div>
</div>
</template>Accessibility
- APG pattern
—