ページネーションPaginationExperimental
複数ページに分かれた一覧や検索結果を移動するためのナビゲーションです。ページ番号、省略記号、前後移動に加えて、件数サマリーや表示件数の選択と組み合わせて使います。
プレビュー
状態とバリエーション
状態管理あり
現在ページをアプリ側の state で管理し、必要なページ番号だけを表示します。
現在のページ 7 / 50
先頭と末尾へ移動
ページ数が多い一覧では、前後移動に加えて先頭と末尾へ直接移動する操作を追加できます。
現在のページ 6 / 24
ラベルなし操作
スペースが限られる場所では、最初・前へ・次へ・最後のラベルを視覚的に隠し、ツールチップと aria-label で意味を補います。
現在のページ 3 / 12
テーブル向け
DataTable と同じ考え方で、件数サマリー、表示件数、先頭/末尾、スマホ用ページ選択を組み合わせます。
プロパティ
表は横にスクロールできます
| プロパティ | 型 | 初期値 | 説明 |
|---|---|---|---|
| Pagination | nav | - | ページ送り全体のランドマーク。 |
| PaginationLink.isActive | boolean | - | 現在ページを示します。 |
| PaginationFirst / PaginationLast | a | - | 必要な場合に先頭ページ、末尾ページへ移動するリンク。長い一覧やテーブルではオプションとして組み合わせます。 |
| PaginationPrevious / PaginationNext | a | - | 前後ページへ移動するリンク。label で表示文言を差し替えられます。 |
| PaginationEllipsis | span | - | 省略されたページ範囲を示します。 |
使い方
import * as React from "react"
import { Pagination, PaginationContent, PaginationEllipsis, PaginationFirst, PaginationItem, PaginationLast, PaginationLink, PaginationNext, PaginationPrevious, Select, Tooltip, TooltipContent, TooltipTrigger } from "@gunjo/ui"
function getPageItems(currentPage: number, pageCount: number): Array<number | "ellipsis-start" | "ellipsis-end"> {
if (pageCount <= 7) return Array.from({ length: pageCount }, (_, index) => index + 1)
if (currentPage <= 4) return [1, 2, 3, 4, 5, "ellipsis-end", pageCount]
if (currentPage >= pageCount - 3) return [1, "ellipsis-start", pageCount - 4, pageCount - 3, pageCount - 2, pageCount - 1, pageCount]
return [1, "ellipsis-start", currentPage - 1, currentPage, currentPage + 1, "ellipsis-end", pageCount]
}
function PaginationControlTooltip({ label, disabledLabel, disabled, children }: { label: string; disabledLabel: string; disabled: boolean; children: React.ReactNode }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">{children}</span>
</TooltipTrigger>
<TooltipContent>{disabled ? disabledLabel : label}</TooltipContent>
</Tooltip>
)
}
function CompactPaginationControls({
page,
totalPages,
onPageChange,
showBoundary = false,
ariaLabel,
}: {
page: number
totalPages: number
onPageChange: (page: number) => void
showBoundary?: boolean
ariaLabel: string
}) {
const canPrevious = page > 1
const canNext = page < totalPages
return (
<Pagination className="sm:hidden" aria-label={ariaLabel}>
<div className="grid w-full max-w-xs grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2">
<div className="flex items-center gap-1">
{showBoundary ? (
<PaginationControlTooltip label="最初のページへ" disabledLabel="すでに最初のページです" disabled={!canPrevious}>
<PaginationFirst href="#" label={<span className="sr-only">最初のページへ</span>} aria-label="最初のページへ" aria-disabled={!canPrevious} className="h-9 w-9 gap-0 px-0" onClick={(event) => { event.preventDefault(); onPageChange(1) }} />
</PaginationControlTooltip>
) : null}
<PaginationControlTooltip label="前へ" disabledLabel="前のページはありません" disabled={!canPrevious}>
<PaginationPrevious href="#" label={<span className="sr-only">前へ</span>} aria-label="前のページへ" aria-disabled={!canPrevious} className="h-9 w-9 gap-0 px-0" onClick={(event) => { event.preventDefault(); onPageChange(Math.max(1, page - 1)) }} />
</PaginationControlTooltip>
</div>
<label className="flex min-w-0 items-center justify-center gap-1 text-sm text-muted-foreground">
<span className="sr-only">ページを選択</span>
<Select aria-label="ページを選択" value={String(page)} onChange={(event) => onPageChange(Number(event.target.value))} className="h-9 w-16 rounded-md py-1 text-center text-sm">
{Array.from({ length: totalPages }, (_, index) => index + 1).map((pageNumber) => (
<option key={pageNumber} value={pageNumber}>{pageNumber}</option>
))}
</Select>
<span aria-hidden="true">/</span>
<span aria-hidden="true">{totalPages}</span>
</label>
<div className="flex items-center justify-end gap-1">
<PaginationControlTooltip label="次へ" disabledLabel="次のページはありません" disabled={!canNext}>
<PaginationNext href="#" label={<span className="sr-only">次へ</span>} aria-label="次のページへ" aria-disabled={!canNext} className="h-9 w-9 gap-0 px-0" onClick={(event) => { event.preventDefault(); onPageChange(Math.min(totalPages, page + 1)) }} />
</PaginationControlTooltip>
{showBoundary ? (
<PaginationControlTooltip label="最後のページへ" disabledLabel="すでに最後のページです" disabled={!canNext}>
<PaginationLast href="#" label={<span className="sr-only">最後のページへ</span>} aria-label="最後のページへ" aria-disabled={!canNext} className="h-9 w-9 gap-0 px-0" onClick={(event) => { event.preventDefault(); onPageChange(totalPages) }} />
</PaginationControlTooltip>
) : null}
</div>
</div>
</Pagination>
)
}
export function PaginationExample() {
const [page, setPage] = React.useState(2)
const totalPages = 10
const pages = React.useMemo(() => getPageItems(page, totalPages), [page, totalPages])
return (
<>
<CompactPaginationControls page={page} totalPages={totalPages} onPageChange={setPage} ariaLabel="ページ送り" />
<Pagination className="hidden sm:flex" aria-label="ページ送り">
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" label="前へ" aria-label="前のページへ" onClick={(event) => { event.preventDefault(); setPage((current) => Math.max(1, current - 1)) }} />
</PaginationItem>
{pages.map((entry) => entry === "ellipsis-start" || entry === "ellipsis-end" ? (
<PaginationItem key={entry}><PaginationEllipsis /></PaginationItem>
) : (
<PaginationItem key={entry}>
<PaginationLink href="#" isActive={entry === page} aria-label={`${entry}ページへ移動`} onClick={(event) => { event.preventDefault(); setPage(entry) }}>
{entry}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext href="#" label="次へ" aria-label="次のページへ" onClick={(event) => { event.preventDefault(); setPage((current) => Math.min(totalPages, current + 1)) }} />
</PaginationItem>
</PaginationContent>
</Pagination>
</>
)
}使用コンポーネント
関連コンポーネント
設計の判断(UIXHERO)
「いつ・なぜ使うか」の判断は、姉妹サイト UIXHERO の記事で解説しています。