Trang này chưa có bản tiếng Việt. Nội dung dưới đây là bản tiếng Anh.

Pagination

Lets users navigate across pages of content with numbered links, previous and next controls, and a truncating ellipsis for long ranges.

Since 3.0.0 the module carries 'use client' — its labels follow ComitorLocaleProvider, and a context can only be read on the client — but a Server Component still renders these primitives as before, because none of their props is a function. They are the scaffolding only: you decide which numbers to show. The version that already knows how to count — page size selector, record range, first/last buttons — is the TablePagination composite, also exported from @comitor/ui.

Basic usage

Compose a Pagination from items, marking the current page with isActive.

Truncated ranges

Drop a PaginationEllipsis between distant page numbers to keep long ranges compact while always showing the first, last, and current pages.

Labels and language

Every string defaults to English: Previous / Next beside the chevrons, “Go to previous page” / “Go to next page” as their accessible names, More pages in the ellipsis, and aria-label="Pagination" on the root. All of them come from the nearest ComitorLocaleProvider: the demo below sits inside locale="vi" and passes no strings at all. Each one is still a plain prop, applied before the spread — so passing your own always wins over the provider.

Routing

PaginationLink has no asChild escape hatch yet — unlike BreadcrumbLink, it always renders its own <a>. Pass href straight through and it behaves like any link; if you need more, wrap the set in a composite of your own at the app layer.

pagination-links.tsx
import { Pagination, PaginationContent, PaginationItem, PaginationLink } from '@comitor/ui'

// PaginationLink has no asChild — it always renders a real <a>. Plain hrefs
// are the simplest route, and they work in a Server Component.
export function Example({ page }: { page: number }) {
  return (
    <Pagination>
      <PaginationContent>
        {[1, 2, 3].map((n) => (
          <PaginationItem key={n}>
            <PaginationLink href={`/invoices?page=${n}`} isActive={n === page} aria-label={`Page ${n}`}>
              {n}
            </PaginationLink>
          </PaginationItem>
        ))}
      </PaginationContent>
    </Pagination>
  )
}

If you want client-side routing and prefetching, render your own Link and reuse the recipe the primitive applies internally — buttonVariants and cn are both exported from the same entry.

pagination-prefetch.tsx
import Link from 'next/link'
import { Pagination, PaginationContent, PaginationItem, buttonVariants, cn } from '@comitor/ui'

// Want next/link prefetching instead? Skip PaginationLink and borrow the
// same recipe it uses internally.
export function Example({ page }: { page: number }) {
  return (
    <Pagination>
      <PaginationContent>
        {[1, 2, 3].map((n) => (
          <PaginationItem key={n}>
            <Link
              href={`/invoices?page=${n}`}
              aria-current={n === page ? 'page' : undefined}
              data-active={n === page}
              className={cn(
                buttonVariants({ variant: 'ghost', size: 'icon' }),
                n === page &&
                  'border-primary-ink bg-brand-50 font-semibold text-foreground hover:bg-brand-50 dark:bg-brand-500/10 dark:hover:bg-brand-500/10'
              )}
            >
              {n}
            </Link>
          </PaginationItem>
        ))}
      </PaginationContent>
    </Pagination>
  )
}

How the current page is marked

The active link gets a faint fill in the theme colour and an accent border, and it is the border that is meant to carry the meaning. bg-brand-50 (gold-50 on the default palette) sits at only 1.09:1 against the page, far under the 3:1 that WCAG 1.4.11 asks of a non-text cue, so on its own it would tell a low-vision reader nothing.

  • border-primary-ink is a role token, not a fixed swatch, so what it draws depends on which of the package's two palettes is active. In the default one --primary-ink points straight at --primary (#E8B824, the same gold as gold-300), which measures 1.86:1 on the page. That is the same figure styles.css records for the Tabs active underline and the input focus ring, among the shortfalls the approved comitor-ds palette knowingly accepts.
  • Under data-contrast="high" the same token repoints to gold-500 (#7A5800): 6.51:1 against the page fill and 5.96:1 against the gold tint it outlines. In dark, --primary-ink follows the lighter --primary (#F5D060) at 12.67:1 in both palettes.
  • That swap is the entire reason the class is border-primary-ink and not border-gold-300. In the default light palette the two render the same pixel today; writing the raw scale step would nail the marker to that palette and leave the high-contrast one silently unfixed.
  • aria-current="page" and data-active are set alongside. On the default palette they are what actually carries the state, so never let the border be a page's only answer to "which page am I on?".

Props

PropKiểuMặc địnhMô tả
PaginationReact.ComponentProps<'nav'>—Root <nav>. Its implicit navigation role is left implicit — no redundant role="navigation" — and it carries aria-label="Pagination" (from ComitorLocaleProvider — “Phân trang” under locale="vi") unless you pass your own.
PaginationContentReact.ComponentProps<'ul'>—The <ul> list that lays out the page items in a horizontal row.
PaginationItemReact.ComponentProps<'li'>—A single <li> wrapper for a link, previous/next control, or ellipsis.
PaginationLink — isActivebooleanfalseMarks the current page: sets aria-current="page", data-active, an accent border and a faint fill in the theme colour.
PaginationLink — size'default' | 'md' | 'sm' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg''icon'Forwarded to buttonVariants, which supplies the ghost-button box. Previous/Next override it to "default" so their text label fits.
PaginationLink — ...restReact.ComponentProps<'a'>—All native anchor props (href, onClick, aria-label…) are forwarded. There is no asChild on this primitive.
PaginationPrevious — labelReact.ReactNode'Previous'Visible label beside the chevron, hidden below the sm breakpoint; null leaves the chevron alone. The accessible name is a separate aria-label, 'Go to previous page' by default. Both follow ComitorLocaleProvider (Trước / Về trang trước under locale="vi").
PaginationNext — labelReact.ReactNode'Next'Same as above for the forward control: aria-label defaults to 'Go to next page' (Sau / Tới trang sau under locale="vi").
PaginationEllipsis — srLabelstring'More pages'Visually hidden text inside the gap indicator; the span itself is aria-hidden. Follows ComitorLocaleProvider (“Thêm trang” under locale="vi").

Accessibility

  • The root renders a <nav> with an aria-label, exposing the control as a discoverable landmark. role="navigation" is not written out — <nav> already has it, and restating a native role is redundant ARIA.
  • The active page link sets aria-current="page" so assistive tech announces the user's current location, and marks it visually with an accent border rather than the tint alone — see above for what that border measures in each palette.
  • PaginationPrevious and PaginationNext carry descriptive aria-labels, keeping the controls readable below the sm breakpoint where the text label is hidden and only the chevron shows.
  • PaginationEllipsis is aria-hidden so the decorative gap is skipped during navigation; its srLabel is the visually-hidden fallback text.
  • Numbered links read as bare digits. Give each one an aria-label such as "Page 3" when the list is announced out of context.