Pagination

Change pages, adjust page size and browse omitted page ranges.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Pagination } from '@hina-ui/vue'

  const page = ref(8)
</script>

<template>
  <Pagination v-model="page" :total="246" />
</template>
vue

Usage

import { Pagination } from '@hina-ui/vue'
ts

Bind the current page with v-model, starting at 1. total is the item count and page-size is the number of items per page. Page and navigation buttons update the current page.

By default, Pagination displays page numbers, previous and next buttons, and interactive ellipses. Information, the size selector and the page input are optional.

Page: 1 / 10

<script setup lang="ts">
  import { ref } from 'vue'
  import { Pagination, Stack, Text } from '@hina-ui/vue'

  const page = ref(1)
</script>

<template>
  <Stack gap="sm">
    <Pagination v-model="page" :total="95" :page-size="10" />
    <Text size="sm" tone="muted">Page: {{ page }} / 10</Text>
  </Stack>
</template>
vue

Examples

Page range

sibling-count sets the number of adjacent pages on each side of the current page. show-edges is enabled by default, keeping the first and last pages visible and using ellipses for omitted ranges.

show-first-last independently enables the first and last arrow buttons. It is off by default.

sibling-count="1"

sibling-count="0" + show-first-last

show-edges="false"

<script setup lang="ts">
  import { ref } from 'vue'
  import { Pagination, Stack, Text } from '@hina-ui/vue'

  const page = ref(10)
</script>

<template>
  <Stack gap="lg">
    <Stack gap="xs">
      <Text size="sm" tone="muted">sibling-count="1"</Text>
      <Pagination v-model="page" :total="200" />
    </Stack>
    <Stack gap="xs">
      <Text size="sm" tone="muted">sibling-count="0" + show-first-last</Text>
      <Pagination v-model="page" :total="200" :sibling-count="0" show-first-last />
    </Stack>
    <Stack gap="xs">
      <Text size="sm" tone="muted">show-edges="false"</Text>
      <Pagination v-model="page" :total="200" :show-edges="false" />
    </Stack>
  </Stack>
</template>
vue

Ellipsis

Click an ellipsis with a mouse to skip 2 × siblingCount + 1 pages in that direction. Hover to open its omitted page range. The list excludes pages already visible in the pagination bar and closes after selection. A touch tap opens the list directly.

Both ellipses share one popup. Switching sides updates its position and range; repeated skips refresh the open range. The popup flips when space is limited. Long ranges scroll and only render choices near the visible area.

During exit, the popup continues to follow its trigger. If the trigger is removed, the popup finishes exiting at its last position.

Current page: 10

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Pagination, Stack, Text } from '@hina-ui/vue'

  const page = ref(10)
  const large = ref(false)
</script>

<template>
  <Stack gap="sm">
    <Pagination v-model="page" :total="large ? 100000 : 250" />
    <Text size="sm" tone="muted">Current page: {{ page }}</Text>
    <Button variant="soft" tone="neutral" class="self-start" @click="large = !large">
      {{ large ? '25 pages' : '10,000 pages' }}
    </Button>
  </Stack>
</template>
vue

Optional controls

show-info displays the item range, total count and page count. Set item-count to the actual number of items shown on the current page. Otherwise the range uses page-size, capped at the total count.

Set page-size-options to display the size selector and bind it with v-model:page-size. Selecting a new size resets the page to 1 and emits change once with the new { page, pageSize }. Use that payload to update data.

show-jump displays a page input. Enter or blur commits the value, clamped to the valid range. Empty or invalid input restores the current page; Escape cancels editing.

change: —

<script setup lang="ts">
  import { ref } from 'vue'
  import { Pagination, Stack, Text, type PaginationChange } from '@hina-ui/vue'

  const page = ref(8)
  const pageSize = ref(10)
  const lastChange = ref<PaginationChange>()
</script>

<template>
  <Stack gap="sm">
    <Pagination
      v-model="page"
      v-model:page-size="pageSize"
      :total="246"
      :page-size-options="[10, 20, 50]"
      show-info
      show-jump
      @change="lastChange = $event"
    />
    <Text size="sm" tone="muted">change: {{ lastChange ?? '—' }}</Text>
  </Stack>
</template>
vue

Total and page size

External changes to total or page-size preserve the current page when it remains valid. Otherwise the page is clamped to the last page and v-model updates. External changes to valid controlled values do not echo a change event.

Page: 18

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Inline, Pagination, Stack, Text } from '@hina-ui/vue'

  const page = ref(18)
  const total = ref(200)
  const pageSize = ref(10)
</script>

<template>
  <Stack gap="sm">
    <Inline gap="sm">
      <Button variant="soft" tone="neutral" @click="total = total === 200 ? 45 : 200">
        total: {{ total }}
      </Button>
      <Button variant="soft" tone="neutral" @click="pageSize = pageSize === 10 ? 25 : 10">
        page-size: {{ pageSize }}
      </Button>
    </Inline>
    <Pagination v-model="page" :total="total" :page-size="pageSize" />
    <Text size="sm" tone="muted">Page: {{ page }}</Text>
  </Stack>
</template>
vue

Layout and composition

align positions the parts with start, center, end or between. Use the default slot to arrange PaginationInfo, PaginationContent, PaginationSize and PaginationJump. These components share their parent’s state, size, disabled state and locale.

The default slot of PaginationInfo receives pagination state for custom text. This example uses Inline to group the controls on the right.

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    Inline,
    Pagination,
    PaginationContent,
    PaginationInfo,
    PaginationJump,
    PaginationSize,
  } from '@hina-ui/vue'

  const page = ref(4)
  const pageSize = ref(10)
</script>

<template>
  <Pagination
    v-model="page"
    v-model:page-size="pageSize"
    :total="180"
    :page-size-options="[10, 20, 50]"
    align="between"
  >
    <PaginationInfo>
      <template #default="{ page: current, pageCount }">{{ current }} / {{ pageCount }}</template>
    </PaginationInfo>
    <Inline gap="sm">
      <PaginationContent />
      <PaginationSize />
      <PaginationJump />
    </Inline>
  </Pagination>
</template>
vue

Loading

pending disables pagination actions and closes the ellipsis list. When #list is provided, its content stays mounted, becomes inert and is covered by LoadingOverlay.

A temporary total of 0 while loading does not overwrite the bound page. Out-of-range pages are corrected after loading ends. This example uses a timer to change loading state; the component does not fetch data.

1

2

3

4

5

<script setup lang="ts">
  import { onBeforeUnmount, ref } from 'vue'
  import { Card, Pagination, Stack, Text, type PaginationChange } from '@hina-ui/vue'

  const page = ref(1)
  const displayedPage = ref(1)
  const pending = ref(false)
  let timer: ReturnType<typeof setTimeout> | undefined

  function change(value: PaginationChange) {
    pending.value = true
    clearTimeout(timer)
    timer = setTimeout(() => {
      displayedPage.value = value.page
      pending.value = false
    }, 1000)
  }

  onBeforeUnmount(() => clearTimeout(timer))
</script>

<template>
  <Pagination
    v-model="page"
    :total="96"
    :page-size="5"
    :pending="pending"
    show-info
    @change="change"
  >
    <template #list>
      <Stack gap="xs">
        <Card v-for="index in 5" :key="index">
          <Text size="sm">{{ (displayedPage - 1) * 5 + index }}</Text>
        </Card>
      </Stack>
    </template>
  </Pagination>
</template>
vue

Hide single-page navigation

hide-single-page hides the navigation when there is at most one page. The #list slot remains mounted.

The list slot remains mounted.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Pagination, Stack, Text } from '@hina-ui/vue'

  const total = ref(5)
</script>

<template>
  <Stack gap="sm">
    <Button variant="soft" tone="neutral" class="self-start" @click="total = total === 5 ? 50 : 5">
      total: {{ total }}
    </Button>
    <Pagination :total="total" hide-single-page>
      <template #list>
        <Text size="sm" tone="muted">The list slot remains mounted.</Text>
      </template>
    </Pagination>
  </Stack>
</template>
vue

Sizes

size accepts sm, md or lg, using the same heights as Button. Page buttons stay square across density settings. Long content is truncated without widening the buttons.

sm

md

lg

<script setup lang="ts">
  import { ref } from 'vue'
  import { Pagination, Stack, Text } from '@hina-ui/vue'

  const page = ref(3)
</script>

<template>
  <Stack gap="lg">
    <Stack v-for="size in ['sm', 'md', 'lg'] as const" :key="size" gap="xs">
      <Text size="sm" tone="muted">{{ size }}</Text>
      <Pagination v-model="page" :total="50" :size="size" />
    </Stack>
  </Stack>
</template>
vue

Truncated content

When page content is truncated, hovering or focusing the button with a keyboard shows the full text in a Tooltip, including custom #page content. An outer TooltipProvider is required; AppShell includes one.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Pagination } from '@hina-ui/vue'

  const page = ref(100000)
</script>

<template>
  <Pagination v-model="page" :total="2000000" size="sm" />
</template>
vue

States

disabled disables every control. Backward controls are disabled on the first page and forward controls on the last page. With total="0", page 1 remains and all direction controls are disabled.

Disabled

total="0"

<script setup lang="ts">
  import { Pagination, Stack, Text } from '@hina-ui/vue'
</script>

<template>
  <Stack gap="lg">
    <Stack gap="xs">
      <Text size="sm" tone="muted">Disabled</Text>
      <Pagination :model-value="3" :total="50" disabled />
    </Stack>
    <Stack gap="xs">
      <Text size="sm" tone="muted">total="0"</Text>
      <Pagination :total="0" show-first-last />
    </Stack>
  </Stack>
</template>
vue

Direction

dir accepts ltr or rtl, taking precedence over Reka’s global direction. Without either, direction is inherited from ancestor elements. RTL mirrors the controls and arrows; next still increases the page number.

LTR

RTL

<script setup lang="ts">
  import { ref } from 'vue'
  import { Pagination, Stack, Text } from '@hina-ui/vue'

  const page = ref(3)
</script>

<template>
  <Stack gap="lg">
    <Stack v-for="dir in ['ltr', 'rtl'] as const" :key="dir" gap="xs">
      <Text size="sm" tone="muted">{{ dir.toUpperCase() }}</Text>
      <Pagination v-model="page" :total="50" :dir="dir" />
    </Stack>
  </Stack>
</template>
vue

Page content

#page receives { page, selected } and replaces button content while preserving interaction and current-page semantics. #ellipsis receives { side, expanded } and replaces ellipsis button content.

This example uses Text to change the selected page’s font weight.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Pagination, Text } from '@hina-ui/vue'

  const page = ref(2)
  const numerals = ['I', 'II', 'III', 'IV', 'V']
</script>

<template>
  <Pagination v-model="page" :total="50">
    <template #page="{ page: number, selected }">
      <Text as="span" :weight="selected ? 'semibold' : 'normal'" class="text-inherit">
        {{ numerals[number - 1] }}
      </Text>
    </template>
  </Pagination>
</template>
vue

Behavior

  • The current page is limited to 1 through the page count. The count is Math.ceil(total / pageSize), with a minimum of 1.
  • A user action that changes the page or size emits change once. Selecting the current value does not emit it. Automatic correction of an out-of-range page also emits change.
  • The size selector always includes the current size and removes duplicate, non-integer and non-positive options.
  • Controls wrap when space is limited. Reduce visible pages with sibling-count and show-edges.

Accessibility

  • The navigation is a nav with a localized name, overridable with label. The current page has aria-current="page".
  • Tab moves between available controls; Enter or Space activates them. Disabled controls are skipped. Buttons do not submit forms.
  • Keyboard focus on an ellipsis opens the list. Enter or Space skips a group; ↓ / ↑ enters at the first / last choice.
  • Within the list, ↑ / ↓ moves between choices, Home / End moves to its boundaries and PageUp / PageDown moves by the visible range. Enter or Space selects.
  • Escape closes the list and restores focus. If selection removes the original ellipsis, focus moves to the current page button. Tab leaves the list in the pagination bar’s order.

API

Props

Prop
Type
Default
Description
modelValue
number
1
Current page; supports v-model
total
number
Required
Total item count
pageSize
number
10
Items per page; supports v-model:page-size
itemCount
number
—
Actual items shown on the current page
siblingCount
number
1
Adjacent page count on either side
showEdges
boolean
true
Keep boundary pages and ellipses
showFirstLast
boolean
false
Show first and last arrow buttons
showInfo
boolean
false
Show pagination information
showJump
boolean
false
Show the page input
pageSizeOptions
number[]
—
Show the size selector when non-empty
hideSinglePage
boolean
false
Hide navigation with at most one page
pending
boolean
false
Loading state
align
'start' | 'center' | 'end' | 'between'
'start'
Alignment of the parts
size
'sm' | 'md' | 'lg'
'md'
Control size
disabled
boolean
false
Disable all controls
dir
'ltr' | 'rtl'
—
Direction; inherited when omitted
label
string
Locale
Accessible name of the navigation
class
string
—
Classes for the root

Other attributes are forwarded to the outer div, which contains the navigation.

Events

Event
Payload
Description
update:modelValue
page: number
User page change or out-of-range correction
update:pageSize
pageSize: number
User page-size change
change
PaginationChange
One effective page or size change

Slots

Slot
Props
Description
default
PaginationState
Compose navigation controls
list
PaginationState
Content above the navigation
page
{ page: number; selected: boolean }
Page button content
ellipsis
{ side: 'prev' | 'next'; expanded: boolean }
Ellipsis button content

Composition and types

PaginationContent renders pages and navigation buttons and accepts the same #page and #ellipsis slots. PaginationInfo provides #default(PaginationState). PaginationSize and PaginationJump render the size selector and page input. All four accept class and must be placed in the default slot of Pagination.

interface PaginationChange {
  page: number
  pageSize: number
}

interface PaginationState extends PaginationChange {
  total: number
  pageCount: number
  from: number
  to: number
}
ts

All components and these types are exported from @hina-ui/vue.