HextaUI

usePagination

Transforma uma página e uma contagem de páginas na lista de páginas e reticências a renderizar, mantendo o comprimento estável conforme a página muda.

siblings
7 items
"use client"

import * as React from "react"
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { usePagination } from "@/hooks/use-pagination"

export function UsePaginationDemo() {
  const [page, setPage] = React.useState(6)
  const [siblings, setSiblings] = React.useState(1)
  const pagination = usePagination({ page, count: 20, siblings })

  return (
    <div className="flex w-full flex-col items-center gap-6">
      <nav aria-label="Pagination" className="flex items-center gap-1">
        <Button
          variant="ghost"
          size="icon-sm"
          aria-label="Previous page"
          disabled={!pagination.hasPrevious}
          onClick={() => setPage(pagination.page - 1)}
        >
          <IconChevronLeft className="rtl:rotate-180" />
        </Button>
        {pagination.items.map((item) =>
          item.type === "ellipsis" ? (
            <span
              key={item.position}
              aria-hidden="true"
              className="w-8 text-center text-sm text-muted-foreground"
            >
              …
            </span>
          ) : (
            <Button
              key={item.page}
              variant={item.page === pagination.page ? "secondary" : "ghost"}
              size="icon-sm"
              aria-current={item.page === pagination.page ? "page" : undefined}
              onClick={() => setPage(item.page)}
            >
              {item.page}
            </Button>
          )
        )}
        <Button
          variant="ghost"
          size="icon-sm"
          aria-label="Next page"
          disabled={!pagination.hasNext}
          onClick={() => setPage(pagination.page + 1)}
        >
          <IconChevronRight className="rtl:rotate-180" />
        </Button>
      </nav>
      <div className="flex items-center gap-3 text-sm text-muted-foreground">
        siblings
        <ToggleGroup
          size="sm"
          variant="outline"
          value={[String(siblings)]}
          onValueChange={(value) => {
            if (value[0]) {
              setSiblings(Number(value[0]))
            }
          }}
        >
          <ToggleGroupItem value="0">0</ToggleGroupItem>
          <ToggleGroupItem value="1">1</ToggleGroupItem>
          <ToggleGroupItem value="2">2</ToggleGroupItem>
        </ToggleGroup>
      </div>
      <code className="font-mono text-xs text-muted-foreground">
        {pagination.items.length} items
      </code>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/use-pagination.json

Adiciona o hook e tudo de que ele depende ao seu projeto.

import { usePagination } from "@/hooks/use-pagination"
const { items, page, hasPrevious, hasNext } = usePagination({
  page: currentPage,
  count: totalPages,
})

items.map((item) =>
  item.type === "ellipsis" ? (
    <span key={item.position}>…</span>
  ) : (
    <a key={item.page} href={`?page=${item.page}`}>{item.page}</a>
  )
)

O hook só faz a conta. Retorna a lista de páginas e reticências a renderizar e deixa a marcação com você, que é como Pagination monta seus links. Use-o para montar seu próprio paginador, como pontos para um carrossel ou um seletor de página no rodapé de uma tabela.

A lista sempre mostra as primeiras e últimas boundaries páginas, e siblings páginas de cada lado da atual. Uma reticência preenche qualquer intervalo de duas páginas ou mais. Um intervalo de exatamente uma página mostra essa página, porque ali a reticência não esconderia mais do que ocupa.

count: 20, siblings: 1, boundaries: 1

page 1    1  2  3  4  5  …  20
page 6    1  …  5  6  7  …  20
page 20   1  …  16 17 18 19 20

Quando há páginas suficientes, a lista sempre tem 2 × boundaries + 2 × siblings + 3 itens. Perto das pontas, a janela se alarga em vez de encolher. Como o comprimento nunca muda, o paginador mantém a largura, e os botões de próximo e anterior ficam sob o ponteiro enquanto você clica pelas páginas.

  • count e page são limitados: uma página além do fim vira a última página, e qualquer coisa que não seja um número finito volta ao padrão.
  • Um count de 0 retorna nenhum item e um page de 0, então uma tabela vazia pode não renderizar nada sem um caso especial.
  • siblings e boundaries vão de 0 a 10.
  • As reticências têm uma position estável de start ou end. Use-a como key do React.

Pontos

Com boundaries: 0 a lista é apenas uma janela em volta da página atual. As reticências viram pontos pequenos, então um conjunto longo de slides nunca precisa de mais de cinco alvos.

"use client"

import * as React from "react"

import { usePagination } from "@/hooks/use-pagination"

export function UsePaginationDots() {
  const [page, setPage] = React.useState(1)
  const { items } = usePagination({
    page,
    count: 12,
    siblings: 1,
    boundaries: 0,
  })

  return (
    <nav aria-label="Slides" className="flex items-center gap-1">
      {items.map((item) =>
        item.type === "ellipsis" ? (
          <span
            key={item.position}
            aria-hidden="true"
            className="size-1 rounded-full bg-border"
          />
        ) : (
          <button
            key={item.page}
            type="button"
            aria-label={`Slide ${item.page}`}
            aria-current={item.page === page ? "true" : undefined}
            onClick={() => setPage(item.page)}
            className="flex size-6 items-center justify-center rounded-full outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
          >
            <span className="h-2 w-2 rounded-full bg-muted-foreground/30 transition-all duration-300 ease-out-quint in-aria-[current=true]:w-5 in-aria-[current=true]:bg-foreground motion-reduce:transition-none" />
          </button>
        )
      )}
    </nav>
  )
}
  • Marque a página atual com aria-current="page" e envolva a lista em um <nav> com um rótulo.
  • Oculte as reticências dos leitores de tela com aria-hidden. Elas não carregam nenhuma informação que os números de página não tenham.
  • Mostre os números de página com tabular-nums para os botões não mudarem de largura quando os dígitos mudam.
PropTipoPadrão
countNúmero total de páginas.
number–
pageA página atual, começando em 1.
number1
siblingsPáginas a mostrar de cada lado da página atual.
number1
boundariesPáginas a mostrar sempre no início e no fim.
number1
PropriedadeDescrição
itemsPaginationItemData[] a renderizar, em ordem.
pageA página atual limitada.
countA contagem de páginas limitada.
hasPreviousSe há uma página antes desta.
hasNextSe há uma página depois desta.
type PaginationItemData =
  | { type: "page"; page: number }
  | { type: "ellipsis"; position: "start" | "end" }

Pagination.