HextaUI

usePagination

ページとページ数から、描画するページと省略記号のリストを生成し、ページが動いても長さを一定に保ちます。

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

フックと、その依存関係をプロジェクトに追加します。

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

このフックは計算だけを行います。描画するページと省略記号のリストを返し、マークアップは任せます。Pagination がリンクを構築しているのもこの方法です。カルーセルのドットや、テーブルフッターのページ選択のような、独自のページャーを作るのに使えます。

リストは、最初と最後の boundaries ページと、現在のページの両側の siblings ページを常に表示します。2ページ以上の隙間は省略記号が埋めます。ちょうど1ページの隙間では、省略記号が占める以上のものを隠さないため、代わりにそのページを表示します。

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

ページが十分にあれば、リストは常に 2 × boundaries + 2 × siblings + 3 個の項目を持ちます。端の付近では、ウィンドウは縮まずに広がります。長さが変わらないため、ページャーの幅は保たれ、クリックして進んでいる間も次へ・前へのボタンはポインターの下に留まります。

  • count と page は範囲内に収められます。末尾を超えたページは最後のページになり、有限の数値でないものはデフォルトにフォールバックします。
  • count が0なら、項目なしで page は0になるため、空のテーブルでも特別なケース処理なしに何も描画しないようにできます。
  • siblings と boundaries は0から10まで。
  • 省略記号は、start または end の安定した position を持ちます。これを React の key として使います。

ドット

boundaries: 0 では、リストは現在のページの周りのウィンドウだけになります。省略記号は小さなドットになるため、長いスライドのセットでも、ターゲットは5つ以上必要ありません。

"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>
  )
}
  • 現在のページを aria-current="page" でマークし、リストをラベル付きの <nav> で囲みます。
  • aria-hidden で省略記号をスクリーンリーダーから隠します。ページ番号が伝える以上の情報はありません。
  • 桁が変わってもボタンの幅が変わらないよう、ページ番号は tabular-nums で表示します。
プロパティ型デフォルト
count総ページ数。
number–
page現在のページ。1から始まります。
number1
siblings現在のページの両側に表示するページ数。
number1
boundaries先頭と末尾に常に表示するページ数。
number1
プロパティ説明
items描画する PaginationItemData[]。順番どおり。
page範囲内に収められた現在のページ。
count範囲内に収められたページ数。
hasPreviousこのページの前にページがあるかどうか。
hasNextこのページの後にページがあるかどうか。
type PaginationItemData =
  | { type: "page"; page: number }
  | { type: "ellipsis"; position: "start" | "end" }

Pagination.