Bỏ qua đến nội dung

Phân trang

Các liên kết trang được đánh số kèm theo các nút điều hướng “trước”/“sau” dành cho kết quả tìm kiếm, bảng dữ liệu và kho lưu trữ — theo phong cách “neobrutalist” với đường viền và bóng đổ.

import {
  Pagination,
  PaginationContent,

Pagination chia một tập kết quả dài thành các liên kết trang được đánh số kèm điều khiển trước và sau. Bên dưới không có primitive nào — một vùng mốc nav bọc các thẻ neo thuần, mỗi thẻ render qua Button và tạo kiểu theo công thức neobrutalist: viền dày, bóng đổ đậm và kiểu chữ đậm.

Nên dùng khi:

  • Trang tìm kiếm và trang danh sách — lưới sản phẩm, bảng tin tuyển dụng, danh bạ; nơi người dùng muốn nhảy thẳng đến trang 12.
  • Bảng dữ liệu — kết hợp biến thể chỉ có biểu tượng với bộ chọn số dòng mỗi trang trong các trang quản trị.
  • Kho lưu trữ blog và tài liệu — các liên kết <a> thật cho mỗi trang một URL riêng mà bộ thu thập dữ liệu đọc được, điều mà cuộn vô hạn không làm nổi.

Cài đặt

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/pagination.json
npx shadcn@latest add https://neobrutalism.com/r/base/pagination.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/pagination.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/pagination.json

Sao chép và dán đoạn mã sau vào dự án của bạn.

components/ui/pagination.tsx
import * as React from "react"
import {
  ChevronLeftIcon,
  ChevronRightIcon,
  MoreHorizontalIcon,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"

function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
  return (
    <nav
      role="navigation"
      aria-label="pagination"
      data-slot="pagination"
      className={cn("mx-auto flex w-full justify-center", className)}
      {...props}
    />
  )
}

function PaginationContent({
  className,
  ...props
}: React.ComponentProps<"ul">) {
  return (
    <ul
      data-slot="pagination-content"
      className={cn("flex items-center gap-0.5", className)}
      {...props}
    />
  )
}

function PaginationItem({ ...props }: React.ComponentProps<"li">) {
  return <li data-slot="pagination-item" {...props} />
}

type PaginationLinkProps = {
  isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
  React.ComponentProps<"a">

function PaginationLink({
  className,
  isActive,
  size = "icon",
  ...props
}: PaginationLinkProps) {
  return (
    <Button
      asChild
      variant={isActive ? "default" : "ghost"}
      size={size}
      className={cn(className)}
    >
      <a
        aria-current={isActive ? "page" : undefined}
        data-slot="pagination-link"
        data-active={isActive}
        {...props}
      />
    </Button>
  )
}

function PaginationPrevious({
  className,
  text = "Previous",
  ...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
  return (
    <PaginationLink
      aria-label="Go to previous page"
      size="default"
      className={cn("pl-1.5!", className)}
      {...props}
    >
      <ChevronLeftIcon data-icon="inline-start" className="cn-rtl-flip" />
      <span className="hidden sm:block">{text}</span>
    </PaginationLink>
  )
}

function PaginationNext({
  className,
  text = "Next",
  ...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
  return (
    <PaginationLink
      aria-label="Go to next page"
      size="default"
      className={cn("pr-1.5!", className)}
      {...props}
    >
      <span className="hidden sm:block">{text}</span>
      <ChevronRightIcon data-icon="inline-end" className="cn-rtl-flip" />
    </PaginationLink>
  )
}

function PaginationEllipsis({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      aria-hidden
      data-slot="pagination-ellipsis"
      className={cn(
        "flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      <MoreHorizontalIcon />
      <span className="sr-only">More pages</span>
    </span>
  )
}

export {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
}

Cập nhật các đường dẫn import cho phù hợp với thiết lập dự án của bạn.

Cách dùng

import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"
<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>
        2
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">3</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" />
    </PaginationItem>
  </PaginationContent>
</Pagination>

Kết hợp

Dùng cách kết hợp sau để xây dựng một Pagination:

Pagination
└── PaginationContent
    ├── PaginationItem
    │   └── PaginationPrevious
    ├── PaginationItem
    │   └── PaginationLink
    ├── PaginationItem
    │   └── PaginationEllipsis
    └── PaginationItem
        └── PaginationNext

Ví dụ

Đơn giản

Một phân trang đơn giản chỉ với các số trang.

import {
  Pagination,
  PaginationContent,

Chỉ biểu tượng

Chỉ dùng các nút trước và sau mà không có số trang. Điều này hữu ích cho các bảng dữ liệu có bộ chọn số hàng trên mỗi trang.

import { Field, FieldLabel } from "@/components/ui/field"
import {
  Pagination,

Next.js

Theo mặc định, component <PaginationLink /> sẽ render một thẻ <a />.

Để dùng component <Link /> của Next.js, hãy thực hiện các thay đổi sau trong pagination.tsx.

+ import Link from "next/link"
 
- type PaginationLinkProps = ... & React.ComponentProps<"a">
+ type PaginationLinkProps = ... & React.ComponentProps<typeof Link>
 
const PaginationLink = ({...props }: ) => (
  <PaginationItem>
-   <a>
+   <Link>
      // ...
-   </a>
+   </Link>
  </PaginationItem>
)
 

RTL

Để bật hỗ trợ RTL trong Neobrutalism, hãy xem hướng dẫn cấu hình RTL.

"use client"

import * as React from "react"

Nhật ký thay đổi

Hỗ trợ RTL

Nếu bạn đang nâng cấp từ một phiên bản trước của component Pagination, bạn sẽ cần áp dụng các thay đổi sau để thêm prop text:

Cập nhật PaginationPrevious.

  function PaginationPrevious({
    className,
+   text = "Previous",
    ...props
- }: React.ComponentProps<typeof PaginationLink>) {
+ }: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
    return (
      <PaginationLink
        aria-label="Go to previous page"
        size="default"
        className={cn("cn-pagination-previous", className)}
        {...props}
      >
        <ChevronLeftIcon />
        <span className="cn-pagination-previous-text hidden sm:block">
-         Previous
+         {text}
        </span>
      </PaginationLink>
    )
  }

Cập nhật PaginationNext.

  function PaginationNext({
    className,
+   text = "Next",
    ...props
- }: React.ComponentProps<typeof PaginationLink>) {
+ }: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
    return (
      <PaginationLink
        aria-label="Go to next page"
        size="default"
        className={cn("cn-pagination-next", className)}
        {...props}
      >
-       <span className="cn-pagination-next-text hidden sm:block">Next</span>
+       <span className="cn-pagination-next-text hidden sm:block">{text}</span>
        <ChevronRightIcon />
      </PaginationLink>
    )
  }

Khả năng truy cập

Pagination là kiểu điều hướng dựa trên liên kết, không phải một widget phức hợp, nên không có mẫu WAI-ARIA riêng — ngữ nghĩa liên kết native đã làm hết phần việc. Phần gốc render một vùng mốc <nav> với aria-label="pagination", PaginationLink đang hoạt động mang aria-current="page", các điều khiển trước/sau đi kèm aria-label tường minh, còn PaginationEllipsis được đặt aria-hidden cùng dòng chữ “More pages” chỉ dành cho trình đọc màn hình.

Tương tác bàn phím chính là hành vi liên kết native của trình duyệt:

PhímHành động
Tab / Shift + TabChuyển focus giữa các liên kết trang và ra ngoài
EnterĐi theo liên kết đang được focus