Files
skills/docs/ui/react/react-ui/src/components/pagination/pagination.md
2026-05-29 08:20:56 +01:00

9.1 KiB

Pagination

(no submodules)

- pagination.variants — `paginationVariants`, `pagina…
  • pagination.variants — paginationVariants, paginationContentVariants, paginationItemVariants, paginationLinkVariants, `paginationPrevN…

  • pagination.variants — paginationVariants, `pagina…

  • pagination.variants — paginationVariants, paginationContentVariants, paginationItemVariants, paginationLinkVariants, `paginationPrevN…

  • pagination.variants — paginationVariants, `pagina…

  • pagination.variants — paginationVariants, paginationContentVariants, paginationItemVariants, paginationLinkVariants, `paginationPrevN…

  • pagination.variants — paginationVariants, `pagina…

  • pagination.variants — paginationVariants, paginationContentVariants, paginationItemVariants, paginationLinkVariants, `paginationPrevN…

  • pagination.variants — paginationVariants, paginationContentVariants, paginationItemVariants, paginationLinkVariants, `paginationPrevN…

Purpose

<Pagination> and its sub-components (<PaginationContent>, <PaginationItem>, <PaginationLink>, <PaginationPrevious>, <PaginationNext>, <PaginationEllipsis>, <PaginationStatus>) are the chassis page-navigation primitive. Pure HTML composition — no Radix wrapper — so the markup is deterministic and trivially server-rendered. The wrapper is a semantic <nav> whose accessible name flows through the bundled react-ui.pagination.label translation key. Inside, an unordered list (<PaginationContent>) carries page link rows (<PaginationLink>) interleaved with previous / next / ellipsis affordances. The current page sets isActive, which adds aria-current="page" so screen-readers announce it as the active page.

The "Page X of Y" status caption (<PaginationStatus>) renders through a real ICU MessageFormat via @suaveplan/i18n MessageParser so the locale's plural rule chooses the right form. Welsh covers six plural categories (zero | one | two | few | many | other), and the chassis test suite exercises each one against the bundled cy-GB template post-wireup.

Features

  • Semantic <nav> + <ul> + <li> markup — pure HTML; SSR-trivial; no JavaScript required for the trail to render.
  • Translated aria-label on the wrapper (react-ui.pagination.label).
  • <PaginationLink isActive> carries aria-current="page" so screen-readers announce the active page.
  • <PaginationPrevious> / <PaginationNext> carry translated aria-labels (react-ui.pagination.previous / react-ui.pagination.next) and RTL-flipped chevron icons.
  • <PaginationEllipsis> provides a collapsed-page indicator with aria-hidden="true" plus a translated aria-label for assistive tech.
  • <PaginationStatus> renders "Page X of Y" via MessageParser ICU plural so locale-appropriate plurals fire — Welsh's six-form plural is exercised end-to-end.
  • Three size variants on <PaginationLink> (default / sm / icon).
  • All sub-components forward refs to their underlying HTML elements.

Usage: Basic Example

import {
    Pagination,
    PaginationContent,
    PaginationEllipsis,
    PaginationItem,
    PaginationLink,
    PaginationNext,
    PaginationPrevious,
} from "@suaveplan/react-ui";

export function PagedList({ page }: { page: number }) {
    return (
        <Pagination>
            <PaginationContent>
                <PaginationItem>
                    <PaginationPrevious href={`?page=${page - 1}`} />
                </PaginationItem>
                {[1, 2, 3].map((n) => (
                    <PaginationItem key={n}>
                        <PaginationLink
                            href={`?page=${n}`}
                            isActive={n === page}
                        >
                            {n}
                        </PaginationLink>
                    </PaginationItem>
                ))}
                <PaginationItem>
                    <PaginationEllipsis />
                </PaginationItem>
                <PaginationItem>
                    <PaginationNext href={`?page=${page + 1}`} />
                </PaginationItem>
            </PaginationContent>
        </Pagination>
    );
}

Usage: Advanced Example

import {
    Pagination,
    PaginationContent,
    PaginationEllipsis,
    PaginationItem,
    PaginationLink,
    PaginationNext,
    PaginationPrevious,
    PaginationStatus,
} from "@suaveplan/react-ui";

interface Props {
    readonly current: number;
    readonly total: number;
    readonly buildHref: (page: number) => string;
}

function paginationWindow(
    current: number,
    total: number
): ReadonlyArray<number | null> {
    if (total <= 7) {
        return Array.from({ length: total }, (_, i) => i + 1);
    }
    if (current <= 3) return [1, 2, 3, 4, null, total - 1, total];
    if (current >= total - 2) {
        return [1, 2, null, total - 3, total - 2, total - 1, total];
    }
    return [1, null, current - 1, current, current + 1, null, total];
}

export function FullPagination({ current, total, buildHref }: Props) {
    const items = paginationWindow(current, total);
    return (
        <div className="flex flex-col items-center gap-2">
            <Pagination>
                <PaginationContent>
                    <PaginationItem>
                        <PaginationPrevious
                            href={buildHref(Math.max(1, current - 1))}
                        />
                    </PaginationItem>
                    {items.map((entry, i) => (
                        <PaginationItem key={`${entry}-${i}`}>
                            {entry === null ? (
                                <PaginationEllipsis />
                            ) : (
                                <PaginationLink
                                    href={buildHref(entry)}
                                    isActive={entry === current}
                                >
                                    {entry}
                                </PaginationLink>
                            )}
                        </PaginationItem>
                    ))}
                    <PaginationItem>
                        <PaginationNext
                            href={buildHref(Math.min(total, current + 1))}
                        />
                    </PaginationItem>
                </PaginationContent>
            </Pagination>
            <PaginationStatus current={current} total={total} />
        </div>
    );
}

API Reference

<Pagination>

Renders a <nav role="navigation"> with a translated aria-label.

Prop Type Default Description
ariaLabelKey TranslationKey react-ui.pagination.label Override the accessible name.

<PaginationContent>, <PaginationItem>

Pure layout — <ul> and <li> containers.

Renders an <a>. Sets aria-current="page" when isActive is true.

Prop Type Default Description
size "default" | "sm" | "icon" "icon" Visual size.
isActive boolean false Marks as the current page.

<PaginationPrevious>, <PaginationNext>

Arrow affordances with translated aria-labels and chevron icons.

Prop Type Default Description
ariaLabelKey TranslationKey react-ui.pagination.previous / .next Override the accessible name.

<PaginationEllipsis>

Collapsed-page indicator. Pure visual filler with aria-hidden="true" and a translated aria-label (react-ui.pagination.morePages).

<PaginationStatus>

The "Page X of Y" caption.

Prop Type Default Description
current number required Active page number.
total number required Total page count.
statusKey TranslationKey react-ui.pagination.pageXofY Override the ICU template key.

The template format is "Page {current} of {total}" in en-GB, with locale-appropriate plural variants in the other bundled locales.

Implementation Notes

  • The chassis NEVER wraps pagination in a Radix primitive — pure HTML keeps SSR trivial and matches shadcn upstream's recommendation.
  • <PaginationPrevious> / <PaginationNext> chevron icons carry rtl:rotate-180 so the arrows flip in RTL contexts.
  • The <PaginationStatus> uses aria-live="polite" and aria-atomic="true" so screen-readers announce updates as the user navigates without interrupting the focus flow.
  • MessageParser is constructed once per render via useMemo so the parser cache is preserved across re-renders.
  • Per the Wave 3d reconciliation finding (#4), the t() call returns the ICU template, then messageParser.format(template, values, { locale }) performs interpolation and plural resolution.
  • Per the Wave 3d reconciliation finding (#2), the active locale is read via the (i18n as { readonly locale: string }).locale cast since useTranslation() throws REACT_I18N.NO_PROVIDER in dev mode if called outside a provider, making the optional-chain branch unreachable.