HextaUI

usePagination

Convierte una página y un número de páginas en la lista de páginas y puntos suspensivos que se renderizan, manteniendo su longitud estable mientras la página cambia.

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

Añade el hook y todo lo que necesita a tu proyecto.

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>
  )
)

El hook solo hace el cálculo. Devuelve la lista de páginas y puntos suspensivos que se renderizan y te deja el marcado, que es como Pagination construye sus enlaces. Úsalo para crear tu propio paginador, como puntos para un carrusel o un selector de página en el pie de una tabla.

La lista siempre muestra las primeras y últimas boundaries páginas, y siblings páginas a cada lado de la actual. Unos puntos suspensivos llenan cualquier hueco de dos páginas o más. Un hueco de exactamente una página muestra esa página, porque unos puntos suspensivos ahí no ocultarían más de lo que ocupan.

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

Cuando hay suficientes páginas, la lista siempre tiene 2 × boundaries + 2 × siblings + 3 elementos. Cerca de los extremos, la ventana se ensancha en lugar de encogerse. Como la longitud nunca cambia, el paginador mantiene su ancho, y los botones siguiente y anterior se quedan bajo el puntero mientras pasas de página.

  • count y page se ajustan: una página más allá del final se convierte en la última página, y cualquier cosa que no sea un número finito vuelve al valor por defecto.
  • Un count de 0 devuelve cero elementos y una page de 0, así que una tabla vacía puede no renderizar nada sin un caso especial.
  • siblings y boundaries van de 0 a 10.
  • Los puntos suspensivos tienen una position estable de start o end. Úsala como key de React.

Puntos

Con boundaries: 0 la lista es solo una ventana alrededor de la página actual. Los puntos suspensivos se convierten en puntos pequeños, así que un conjunto largo de diapositivas nunca necesita más de cinco destinos.

"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>
  )
}
  • Marca la página actual con aria-current="page" y envuelve la lista en un <nav> con una etiqueta.
  • Oculta los puntos suspensivos a los lectores de pantalla con aria-hidden. No aportan información que los números de página no den.
  • Muestra los números de página con tabular-nums para que los botones no cambien de ancho al cambiar los dígitos.
PropTipoPredeterminado
countNúmero total de páginas.
number–
pageLa página actual, empezando en 1.
number1
siblingsPáginas que se muestran a cada lado de la página actual.
number1
boundariesPáginas que se muestran siempre al principio y al final.
number1
PropiedadDescripción
itemsPaginationItemData[] que se renderiza, en orden.
pageLa página actual ajustada.
countEl número de páginas ajustado.
hasPreviousSi hay una página antes de esta.
hasNextSi hay una página después de esta.
type PaginationItemData =
  | { type: "page"; page: number }
  | { type: "ellipsis"; position: "start" | "end" }

Pagination.