Pagination

Pagination with page navigation, next and previous links.

Examples

Basic

PropDefaultTypeDescription
total0numberThe total number of items in your list.
defaultPage1numberThe value of the page that should be active when initially rendered. Use when you do not need to control the value state.
disabledfalsebooleanWhen true, prevents the user from interacting with item.
itemsPerPage10numberNumber of items per page.
page-numberThe controlled value of the current page. Can be binded as v-model:page.
showEdgesfalsebooleanWhen true, always show first page, last page, and ellipsis.
Preview
Code

Sibling Count

PropDefaultTypeDescription
siblingCount2numberThe number of surrounding pages displayed around the current page.
Preview
Code

Visibility

PropDefaultTypeDescription
showFirsttruebooleanDisplays the first page button.
showLasttruebooleanDisplays the last page button.
showPrevtruebooleanDisplays the previous page button.
showNexttruebooleanDisplays the next page button.
showListItemtruebooleanDisplays the list items.
Preview
Code
Current Page: 1

Size

PropDefaultTypeDescription
sizesmstringAdjusts the size of the entire pagination.
firstPageSizesmstringCustomizes the size of the first page button.
lastPageSizesmstringCustomizes the size of the last page button.
prevPageSizesmstringCustomizes the size of the previous page button.
nextPageSizesmstringCustomizes the size of the next page button.
listItemSizesmstringCustomizes the size of the page list items.
ellipsisSizesmstringCustomizes the size of the ellipsis indicator.
Preview
Code

Variant and Color

PropDefaultTypeDescription
pagination-selectedsolid-primary{variant}-{color}The color of the selected page.
pagination-unselectedoutline-gray{variant}-{color}The color of the unselected page.
pagination-ellipsistext-black{variant}-{color}The color of the ellipsis.
Preview
Code

Rounded

Preview
Code

Sub Components

PropAPI referenceDescription
_pagination-list-itemReka Pagination ListItem APICustomizes the pagination list item component.
_pagination-prevReka Pagination Prev APICustomizes the previous page navigation button.
_pagination-nextReka Pagination Next APICustomizes the next page navigation button.
_pagination-firstReka Pagination First APICustomizes the first page navigation button.
_pagination-lastReka Pagination Last APICustomizes the last page navigation button.
_pagination-ellipsisReka Pagination Ellipsis APICustomizes the ellipsis indicator in the pagination.
_pagination-listReka Pagination List APICustomizes the pagination list component.
_pagination-infoPage metaCustomizes the page info text.
_pagination-rows-per-pagePage metaCustomizes the rows-per-page select.
Preview
Code

Slots

NamePropsDescription
first-Customizes the first page navigation button.
last-Customizes the last page navigation button.
prev-Customizes the previous page navigation button.
next-Customizes the next page navigation button.
list-itemitem, pageCustomizes the pagination list item component.
ellipsis-Customizes the ellipsis indicator in the pagination.
start-Region before the page list. Holds the page meta and rows-per-page control.
end-Region after the page list.

start and end sit inside the root but outside the page list, so their content is not announced as part of it. They are named for logical order, so they follow the reading direction under RTL. The root only becomes a flex row when one of them actually renders — a pagination without regions lays out exactly as it did before.

Preview
Code

The regions take anything. Here start holds NPaginationInfo, its scoped slot rewording the range, and end holds a button:

Preview
Code

Page meta

Two opt-in parts render into the start region. Both default to false, so an existing NPagination renders exactly what it did before.

PropDefaultTypeDescription
showInfofalsebooleanRender NPaginationInfo.
showRowsPerPagefalsebooleanRender NPaginationRowsPerPage.
_paginationInfo{}objectProps for the info part.
_paginationRowsPerPage{}objectProps for the rows-per-page control.
Preview
Code
Page 1, 10 per page

Info

NPaginationInfo renders where the reader is as text.

PropDefaultTypeDescription
formatpagepage, range or totalPage 1 of 5, Showing 1–10 of 50 or 50 items.
page, pageCount, total, itemsPerPage-numberStandalone fallbacks; read from context inside NPagination.

range and total need total, which is optional on the root, so both fall back to the page form when it is absent. For any other wording — or another language, since the built-in strings are English — its default slot exposes page, pageCount, total, itemsPerPage, first and last. The page is clamped into 1…pageCount before anything is rendered, so a page size that shrinks the page count never prints an impossible range.

Rows per page

NPaginationRowsPerPage wraps NSelect over a list of page sizes.

PropDefaultTypeDescription
pageSizes[10, 20, 30, 40, 50]number[]The sizes to offer.
label-stringCaption rendered before the select. Also available as a slot.
itemsPerPage-numberStandalone fallback; read from context inside NPagination.
@update:itemsPerPage-numberEmitted with the chosen size.

Every other NSelect prop passes through — _selectTrigger, placeholder, disabled and so on. Inside NPagination it writes back through v-model:items-per-page.

Both parts also work on their own, outside NPagination, by passing the values they would otherwise read from context:

<NPaginationInfo
  format="range"
  :page="2"
  :page-count="5"
  :items-per-page="10"
  :total="42"
/>

<NPaginationRowsPerPage
  label="Rows per page"
  :items-per-page="size"
  @update:items-per-page="n => size = n"
/>

Presets

shortcuts/pagination.ts
type PaginationPrefix = 'pagination'

export const staticPagination: Record<`${PaginationPrefix}-${string}` | PaginationPrefix, string> = {
  // configurations
  'pagination': 'overflow-hidden',
  // `shrink-0` because the list already clips: squeezing it between a start
  // region and the edge would silently hide page buttons
  'pagination-list': 'flex shrink-0 items-center gap-1 overflow-hidden',

  // components
  'pagination-root': '',
  // applied only when a start/end region actually renders — `w-full` would
  // otherwise change every existing pagination from shrink-to-fit to full
  // width and re-flow it wherever it sits as a flex item
  'pagination-root-regions': 'flex w-full flex-wrap items-center justify-between gap-4',
  'pagination-start': 'flex min-w-0 items-center gap-2',
  'pagination-end': 'flex items-center gap-2',
  'pagination-info': 'text-sm text-muted-foreground',
  'pagination-rows-per-page': 'flex items-center gap-2',
  'pagination-rows-per-page-label': 'text-sm font-medium whitespace-nowrap',
  'pagination-list-item': 'pagination',

  'pagination-ellipsis-base': 'btn-(~ square) flex items-center justify-center',
  'pagination-ellipsis-icon-base': 'square-1.25em',
  'pagination-ellipsis-icon': 'i-lucide-ellipsis',

  'pagination-first': 'pagination',
  'pagination-first-icon': 'i-lucide-chevrons-left',

  'pagination-prev': 'pagination',
  'pagination-prev-icon': 'i-lucide-chevron-left',

  'pagination-next': 'pagination',
  'pagination-next-icon': 'i-lucide-chevron-right',

  'pagination-last': 'pagination',
  'pagination-last-icon': 'i-lucide-chevrons-right',
}

export const dynamicPagination = [
  [
    /^pagination-ellipsis(?:-([^-]+))?(?:-([^-]+))?$/,
    ([, variant = 'text', color = 'black']) =>
      `btn-${variant}-${color}`,
  ],

  [
    /^pagination-selected(?:-([^-]+))?(?:-([^-]+))?$/,
    ([, variant = 'solid', color = 'primary']) =>
      `data-[selected=true]:btn-${variant}-${color}`,
  ],
  [
    /^pagination-unselected(?:-([^-]+))?(?:-([^-]+))?$/,
    ([, variant = 'outline', color = 'gray']) =>
      `data-[selected=false]:btn-${variant}-${color}`,
  ],

]

export const pagination = [
  ...dynamicPagination,
  staticPagination,
]

Props

types/pagination.ts
import type { PaginationEllipsisProps, PaginationFirstProps, PaginationLastProps, PaginationListItemProps, PaginationListProps, PaginationNextProps, PaginationPrevProps, PaginationRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { NButtonProps } from './button'
import type { NSelectProps } from './select'

interface BaseExtensionProps {
  square?: HTMLAttributes['class'] | boolean
  class?: HTMLAttributes['class']
  rounded?: HTMLAttributes['class']
  size?: HTMLAttributes['class']
}

type isVisible = boolean

export interface NPaginationProps extends
  PaginationRootProps,
  BaseExtensionProps,
  Pick<NButtonProps, 'paginationSelected' | 'paginationUnselected'>,
  Pick<NPaginationEllipsisProps, 'paginationEllipsis'> {
  showFirst?: isVisible
  showPrev?: isVisible
  showNext?: isVisible
  showLast?: isVisible
  showListItem?: isVisible
  /**
   * Render `NPaginationInfo` in the start region.
   *
   * Defaults to `false`, unlike the flags above — the part is new, and
   * turning it on by default would add page meta to every existing usage.
   */
  showInfo?: isVisible
  /**
   * Render `NPaginationRowsPerPage` in the start region.
   *
   * @default false
   */
  showRowsPerPage?: isVisible

  // sub-components
  _paginationList?: Partial<NPaginationListProps>
  _paginationListItem?: Partial<NPaginationListItemProps>
  _paginationEllipsis?: Partial<NPaginationEllipsisProps>
  _paginationFirst?: Partial<NPaginationFirstProps>
  _paginationPrev?: Partial<NPaginationPrevProps>
  _paginationNext?: Partial<NPaginationNextProps>
  _paginationLast?: Partial<NPaginationLastProps>
  _paginationInfo?: Partial<NPaginationInfoProps>
  _paginationRowsPerPage?: Partial<NPaginationRowsPerPageProps>

  una?: NPaginationUnaProps
}

export interface NPaginationInfoProps extends BaseExtensionProps {
  /**
   * Which shape the default text takes.
   *
   * `range` and `total` need `total`, which is optional on the root — both
   * fall back to `page` when it is absent.
   *
   * @default 'page'
   */
  format?: 'page' | 'range' | 'total'

  // standalone fallbacks; supplied by context when nested in `NPagination`
  page?: number
  pageCount?: number
  total?: number
  itemsPerPage?: number

  una?: Pick<NPaginationUnaProps, 'pagination' | 'paginationInfo'>
}

export interface NPaginationRowsPerPageProps extends Omit<NSelectProps<number>, 'una' | 'items' | 'label' | 'modelValue' | 'defaultValue'> {
  /**
   * The page sizes to offer.
   *
   * @default [10, 20, 30, 40, 50]
   */
  pageSizes?: number[]
  /**
   * Caption rendered before the select, e.g. `"Rows per page"`.
   */
  label?: string
  /** Standalone fallback; supplied by context when nested in `NPagination`. */
  itemsPerPage?: number

  una?: Pick<NPaginationUnaProps, 'pagination' | 'paginationRowsPerPage' | 'paginationRowsPerPageLabel'> & NSelectProps<number>['una']
}

export interface NPaginationListProps extends PaginationListProps, BaseExtensionProps {
  una?: Pick<NPaginationUnaProps, 'paginationList'>
}

export interface NPaginationListItemProps extends PaginationListItemProps, Omit<NButtonProps, 'una'> {
  isSelected?: boolean
  page?: PaginationRootProps['page']

  una?: Pick<NPaginationUnaProps, 'paginationListItem' | 'pagination'> & NButtonProps['una']
}

export interface NPaginationEllipsisProps extends PaginationEllipsisProps, BaseExtensionProps {
  paginationEllipsis?: HTMLAttributes['class']

  una?: Pick<NPaginationUnaProps, 'paginationEllipsis' | 'paginationEllipsisIconBase' | 'paginationEllipsisIcon' | 'pagination'>
}

export interface NPaginationFirstProps extends PaginationFirstProps, Omit<NButtonProps, 'una'> {
  una?: Pick<NPaginationUnaProps, 'paginationFirst' | 'pagination'> & NButtonProps['una']
}

export interface NPaginationPrevProps extends PaginationPrevProps, Omit<NButtonProps, 'una'> {
  una?: Pick<NPaginationUnaProps, 'paginationPrev' | 'pagination'> & NButtonProps['una']
}

export interface NPaginationNextProps extends PaginationNextProps, Omit<NButtonProps, 'una'> {
  una?: Pick<NPaginationUnaProps, 'paginationNext' | 'pagination'> & NButtonProps['una']
}

export interface NPaginationLastProps extends PaginationLastProps, Omit<NButtonProps, 'una'> {
  una?: Pick<NPaginationUnaProps, 'paginationLast' | 'pagination'> & NButtonProps['una']
}

interface NPaginationUnaProps {
  pagination?: HTMLAttributes['class']
  paginationRoot?: HTMLAttributes['class']
  paginationList?: HTMLAttributes['class']
  paginationNext?: HTMLAttributes['class']
  paginationPrev?: HTMLAttributes['class']
  paginationFirst?: HTMLAttributes['class']
  paginationLast?: HTMLAttributes['class']
  paginationListItem?: HTMLAttributes['class']
  paginationEllipsis?: HTMLAttributes['class']
  paginationEllipsisIconBase?: HTMLAttributes['class']
  paginationEllipsisIcon?: HTMLAttributes['class']

  // regions and meta parts
  paginationStart?: HTMLAttributes['class']
  paginationEnd?: HTMLAttributes['class']
  paginationInfo?: HTMLAttributes['class']
  paginationRowsPerPage?: HTMLAttributes['class']
  paginationRowsPerPageLabel?: HTMLAttributes['class']
}

Components

Pagination.vue
PaginationEllipsis.vue
PaginationListItem.vue
PaginationFirst.vue
PaginationLast.vue
PaginationNext.vue
PaginationPrev.vue
PaginationInfo.vue
PaginationRowsPerPage.vue
<script setup lang="ts">
import type { PaginationRootEmits } from 'reka-ui'
import type { NPaginationProps } from '../../../types'
import { reactivePick } from '@vueuse/core'
import { PaginationList, PaginationRoot, useForwardPropsEmits } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../../utils'
import PaginationEllipsis from './PaginationEllipsis.vue'
import PaginationFirst from './PaginationFirst.vue'
import PaginationInfo from './PaginationInfo.vue'
import PaginationLast from './PaginationLast.vue'
import PaginationListItem from './PaginationListItem.vue'
import PaginationNext from './PaginationNext.vue'
import PaginationPrev from './PaginationPrev.vue'
import PaginationRowsPerPage from './PaginationRowsPerPage.vue'
import { providePaginationContext } from './usePagination'

const props = withDefaults(defineProps<NPaginationProps>(), {
  showFirst: true,
  showLast: true,
  showListItem: true,
  showNext: true,
  showPrev: true,
  showInfo: false,
  showRowsPerPage: false,
  square: undefined,
})

const emits = defineEmits<PaginationRootEmits & {
  'update:itemsPerPage': [value: number]
}>()

const slots = defineSlots()

const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultPage', 'disabled', 'itemsPerPage', 'page', 'showEdges', 'siblingCount', 'total'), emits)

// The root only becomes a flex row when there is something to lay out beside
// the list. Applying the layout unconditionally gave every existing
// `NPagination` `width: 100%`, which re-flows it wherever it sits as a flex
// item beside a sibling label — the shape used throughout the table examples.
//
// Plain functions, not computeds: `slots` is an object Vue mutates in place on
// re-render rather than a reactive source, so a computed reading `slots.start`
// caches its first result forever and a conditionally-provided `#start` would
// never appear. Functions re-evaluate with each render, which is what a slot
// check needs.
function hasStart() {
  return Boolean(slots.start || props.showInfo || props.showRowsPerPage)
}
function hasEnd() {
  return Boolean(slots.end)
}
function hasRegions() {
  return hasStart() || hasEnd()
}

// reka's root context already carries `page` and `pageCount`; this supplies
// what it lacks, so `itemsPerPage` gains a write path it has no emit for
providePaginationContext({
  total: computed(() => props.total),
  itemsPerPage: computed(() => props.itemsPerPage),
  onItemsPerPageChange: (value: number) => emits('update:itemsPerPage', value),
})
</script>

<template>
  <PaginationRoot
    v-slot="{ page }"
    v-bind="rootProps"
    :class="cn(
      'pagination-root',
      hasRegions() && 'pagination-root-regions',
      props.class,
      props.una?.paginationRoot,
    )"
  >
    <div
      v-if="hasStart()"
      :class="cn('pagination-start', props.una?.paginationStart)"
    >
      <slot name="start">
        <PaginationInfo
          v-if="showInfo"
          :una
          v-bind="_paginationInfo"
        />

        <PaginationRowsPerPage
          v-if="showRowsPerPage"
          :disabled
          :una
          v-bind="_paginationRowsPerPage"
        />
      </slot>
    </div>

    <PaginationList
      v-slot="{ items }"
      :class="cn(
        'pagination-list',
        props?._paginationList?.class,
        props.una?.paginationList,
      )"
      v-bind="_paginationList"
    >
      <slot>
        <PaginationFirst
          v-if="showFirst"
          :rounded
          :size
          :pagination-selected
          :pagination-unselected
          v-bind="_paginationFirst"
          :square
          :una
        >
          <slot
            name="first"
          />
        </PaginationFirst>

        <PaginationPrev
          v-if="showPrev"
          :rounded
          :pagination-selected
          :pagination-unselected
          :size
          v-bind="_paginationPrev"
          :square
          :una
        >
          <slot
            name="prev"
          />
        </PaginationPrev>

        <template v-if="showListItem">
          <template v-for="(item, index) in items">
            <slot
              v-if="item.type === 'page'"
              name="list-item"
              :item="item"
              :page="page"
            >
              <PaginationListItem
                :key="index"
                :value="item.value"
                :page
                :rounded
                :size
                :pagination-selected
                :pagination-unselected
                :square
                :una
                v-bind="_paginationListItem"
              />
            </slot>

            <PaginationEllipsis
              v-else
              :key="item.type"
              :index="index"
              :rounded
              :size
              :pagination-ellipsis
              :square
              :una
              v-bind="_paginationEllipsis"
            >
              <slot
                name="ellipsis"
              />
            </PaginationEllipsis>
          </template>
        </template>

        <PaginationNext
          v-if="showNext"
          :rounded
          :size
          :pagination-selected
          :pagination-unselected
          v-bind="_paginationNext"
          :square
          :una
        >
          <slot
            name="next"
          />
        </PaginationNext>

        <PaginationLast
          v-if="showLast"
          :rounded
          :size
          :pagination-selected
          :pagination-unselected
          :square
          :una
          v-bind="_paginationLast"
        >
          <slot
            name="last"
          />
        </PaginationLast>
      </slot>
    </PaginationList>

    <div
      v-if="hasEnd()"
      :class="cn('pagination-end', props.una?.paginationEnd)"
    >
      <slot name="end" />
    </div>
  </PaginationRoot>
</template>