HextaUI

usePagination

Transforme une page et un nombre de pages en liste de pages et de points de suspension à afficher, en gardant une longueur stable quand la page change.

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

Ajoute le hook et tout ce dont il dépend à votre projet.

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

Le hook ne fait que le calcul. Il renvoie la liste des pages et des points de suspension à rendre et vous laisse le balisage, ce qui est la manière dont Pagination construit ses liens. Utilisez-le pour construire votre propre pagination, comme des points pour un carrousel ou un sélecteur de page dans le pied d'un tableau.

La liste affiche toujours les boundaries premières et dernières pages, et siblings pages de chaque côté de la page courante. Des points de suspension comblent tout écart de deux pages ou plus. Un écart d'exactement une page affiche cette page à la place, car des points de suspension n'y cacheraient pas plus qu'ils ne prennent de place.

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

Dès qu'il y a assez de pages, la liste contient toujours 2 × boundaries + 2 × siblings + 3 éléments. Près des extrémités, la fenêtre s'élargit au lieu de rétrécir. Comme la longueur ne change jamais, la pagination garde sa largeur, et les boutons suivant et précédent restent sous le pointeur pendant que vous cliquez.

  • count et page sont bornés : une page au-delà de la fin devient la dernière page, et tout ce qui n'est pas un nombre fini revient à la valeur par défaut.
  • Un count de 0 renvoie aucun élément et une page de 0, si bien qu'un tableau vide peut ne rien afficher sans cas particulier.
  • siblings et boundaries vont de 0 à 10.
  • Les points de suspension ont une position stable, start ou end. Utilisez-la comme key React.

Points

Avec boundaries: 0, la liste n'est qu'une fenêtre autour de la page courante. Les points de suspension deviennent de petits points, si bien qu'une longue série de diapositives n'a jamais besoin de plus de cinq cibles.

"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>
  )
}
  • Marquez la page courante avec aria-current="page" et entourez la liste d'un <nav> avec un label.
  • Masquez les points de suspension aux lecteurs d'écran avec aria-hidden. Ils ne portent aucune information que les numéros de page n'apportent pas.
  • Affichez les numéros de page avec tabular-nums pour que les boutons ne changent pas de largeur quand les chiffres changent.
PropTypePar défaut
countNombre total de pages.
number–
pageLa page courante, à partir de 1.
number1
siblingsPages à afficher de chaque côté de la page courante.
number1
boundariesPages à toujours afficher au début et à la fin.
number1
PropriétéDescription
itemsPaginationItemData[] à rendre, dans l'ordre.
pageLa page courante bornée.
countLe nombre de pages borné.
hasPreviousIndique s'il existe une page avant celle-ci.
hasNextIndique s'il existe une page après celle-ci.
type PaginationItemData =
  | { type: "page"; page: number }
  | { type: "ellipsis"; position: "start" | "end" }

Pagination.