HextaUI

usePagination

Macht aus einer Seite und einer Seitenzahl die Liste der zu rendernden Seiten und Ellipsen und hält ihre Länge beim Blättern konstant.

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

Fügt den Hook und alles, wovon er abhängt, zu deinem Projekt hinzu.

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

Der Hook rechnet nur. Er gibt die Liste der zu rendernden Seiten und Ellipsen zurück und überlässt dir das Markup, so baut Pagination seine Links. Nutze ihn, um einen eigenen Pager zu bauen, etwa Punkte für ein Karussell oder eine Seitenauswahl in einem Tabellenfuß.

Die Liste zeigt immer die ersten und letzten boundaries Seiten und siblings Seiten auf jeder Seite der aktuellen. Eine Ellipse füllt jede Lücke von zwei oder mehr Seiten. Eine Lücke von genau einer Seite zeigt stattdessen diese Seite, da eine Ellipse dort nicht mehr verbergen würde, als sie selbst einnimmt.

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

Sobald es genug Seiten gibt, hat die Liste immer 2 × boundaries + 2 × siblings + 3 Items. Nahe den Enden weitet sich das Fenster, statt zu schrumpfen. Da sich die Länge nie ändert, behält der Pager seine Breite, und die Buttons für Weiter und Zurück bleiben beim Durchklicken unter dem Zeiger.

  • count und page werden begrenzt: Eine Seite hinter dem Ende wird zur letzten Seite, und alles, was keine endliche Zahl ist, fällt auf den Standard zurück.
  • Ein count von 0 gibt keine Items und eine page von 0 zurück, sodass eine leere Tabelle ohne Sonderfall nichts rendern kann.
  • siblings und boundaries reichen von 0 bis 10.
  • Ellipsen haben eine stabile position von start oder end. Nutze sie als React-Key.

Punkte

Mit boundaries: 0 ist die Liste nur ein Fenster um die aktuelle Seite. Ellipsen werden zu kleinen Punkten, sodass eine lange Reihe von Slides nie mehr als fünf Ziele braucht.

"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>
  )
}
  • Markiere die aktuelle Seite mit aria-current="page" und umschließe die Liste mit einem <nav> mit Label.
  • Verberge Ellipsen mit aria-hidden vor Screenreadern. Sie tragen keine Information, die die Seitenzahlen nicht auch tragen.
  • Zeige Seitenzahlen mit tabular-nums, damit die Buttons ihre Breite nicht ändern, wenn sich die Ziffern ändern.
PropTypStandard
countGesamtzahl der Seiten.
number–
pageDie aktuelle Seite, bei 1 beginnend.
number1
siblingsSeiten, die auf jeder Seite der aktuellen Seite angezeigt werden.
number1
boundariesSeiten, die immer am Anfang und am Ende angezeigt werden.
number1
PropertyBeschreibung
itemsPaginationItemData[], der Reihe nach zu rendern.
pageDie begrenzte aktuelle Seite.
countDie begrenzte Seitenanzahl.
hasPreviousOb es eine Seite vor dieser gibt.
hasNextOb es eine Seite nach dieser gibt.
type PaginationItemData =
  | { type: "page"; page: number }
  | { type: "ellipsis"; position: "start" | "end" }

Pagination.