Skip to main content

Pagination

Pagination is a navigation control that lets people move through pages of content, such as search results or invoices.

Import

import { Pagination } from '@vortexlabs/vortex';
import type { PaginationProps } from '@vortexlabs/vortex';

Other links

<Stack
  width="100%"
  attributes={{"style":{"overflowX":"auto"}}}
>
  <Pagination
    total={10}
    defaultPage={5}
    ariaLabel="Pagination playground"
    previousAriaLabel="Previous page"
    nextAriaLabel="Next page"
  />
</Stack>

Features

  • Offers numbered and compact layouts.
  • Supports controlled and uncontrolled page selection.
  • Validates typed page numbers before changing pages.
  • Supports native links and router links.

Numbered Pages

Set total to the page count and defaultPage to the starting page, counted from 1. Use siblingCount for neighboring page numbers; the default is 1 on each side.

"use client";

import { Pagination } from "@vortexlabs/vortex";

export const PaginationNumberedExample = () => (
  <Pagination
    ariaLabel="Pagination: numbered"
    previousAriaLabel="Previous page"
    nextAriaLabel="Next page"
    total={10}
    defaultPage={5}
  />
);

Compact Layout

Use variant="compact" for a page count between the arrows. Provide pageLabel and totalLabel for its text.

"use client";

import { Pagination } from "@vortexlabs/vortex";

export const PaginationCompactExample = () => (
  <Pagination
    ariaLabel="Pagination: compact text"
    previousAriaLabel="Previous page"
    nextAriaLabel="Next page"
    variant="compact"
    pageLabel="Page"
    totalLabel={({ total }) => `of ${total}`}
    pageInput={false}
    total={10}
    defaultPage={5}
  />
);

Page Input

Set pageInput in compact mode to accept a page number on Enter. Provide its ariaLabel and errorMessage; invalid entries show the error without changing pages.

"use client";

import { Pagination } from "@vortexlabs/vortex";

export const PaginationInputExample = () => (
  <Pagination
    ariaLabel="Pagination: page input and validation"
    previousAriaLabel="Previous page"
    nextAriaLabel="Next page"
    variant="compact"
    pageLabel="Page"
    totalLabel={({ total }) => `of ${total}`}
    pageInput={{
      ariaLabel: "Page number",
      errorMessage: ({ total }) => `Enter a whole number from 1 to ${total}.`,
    }}
    total={2500}
    defaultPage={1000}
  />
);

With one page, the input becomes text.

Controlled Selection

Use page with onChange to control selection. Update page in the handler and load or display its content in your app.

Selected page: 3
"use client";

import { Button, Pagination, Stack, Text } from "@vortexlabs/vortex";
import React from "react";

export const PaginationControlledExample = () => {
  const [page, setPage] = React.useState(3);

  return (
    <Stack gap={3}>
      <Text size="body-sm-desktop" attributes={{ "aria-live": "polite" }}>
        Selected page: {page}
      </Text>
      <Pagination
        ariaLabel="Pagination: controlled"
        previousAriaLabel="Previous page"
        nextAriaLabel="Next page"
        total={8}
        page={page}
        onChange={({ page }) => setPage(page)}
      />
      <Stack direction="row">
        <Button variant="secondary" size="sm" onClick={() => setPage(1)}>
          Reset to page 1
        </Button>
      </Stack>
    </Stack>
  );
};

Use href for page URLs and page for the current page. Omit onChange and defaultPage in link mode.

Use renderLink for router links; forward all supplied attributes, including ref and children.

Loading pagination…
"use client";

import { Pagination } from "@vortexlabs/vortex";
import NextLink from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import { Suspense } from "react";

const PaginationLinks = () => {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const requestedPage = Number(searchParams.get("page") ?? 1);
  const page = Number.isInteger(requestedPage)
    ? Math.min(5, Math.max(1, requestedPage))
    : 1;

  return (
    <Pagination
      total={5}
      page={page}
      href={({ page }) => `${pathname}?page=${page}#link-navigation`}
      renderLink={(attributes) => <NextLink {...attributes} />}
      ariaLabel="Pagination: linked pages"
      previousAriaLabel="Previous page"
      nextAriaLabel="Next page"
    />
  );
};

export const PaginationLinksExample = () => (
  <Suspense fallback={<span>Loading pagination…</span>}>
    <PaginationLinks />
  </Suspense>
);

Omit renderLink for native links. In link mode, page input submissions use window.location.assign, even with a custom renderLink.

Full Width

Set fullWidth in numbered mode to place the arrows at the container's edges.

"use client";

import { Pagination } from "@vortexlabs/vortex";

export const PaginationWidthExample = () => (
  <Pagination
    ariaLabel="Pagination: full width"
    previousAriaLabel="Previous page"
    nextAriaLabel="Next page"
    total={10}
    defaultPage={5}
    fullWidth
  />
);

Set total to Math.ceil(totalRows / rowsPerPage) for a table footer. Reset page to 1 when changing the page size.

101–125 of 2,500 rows
"use client";

import { Pagination, Select, Stack, Text } from "@vortexlabs/vortex";
import React from "react";

export const PaginationFooterExample = () => {
  const [page, setPage] = React.useState(5);
  const [rowsPerPage, setRowsPerPage] = React.useState(25);
  const totalRows = 2500;
  const firstRow = (page - 1) * rowsPerPage + 1;
  const lastRow = Math.min(page * rowsPerPage, totalRows);

  return (
    <Stack
      direction="row"
      align="start"
      justify="space-between"
      gap={4}
      padding={4}
      borderColor="default"
      attributes={{ style: { borderColor: "var(--vortex-color-border-divider)" } }}
      wrap
    >
      <Stack minHeight={10} justify="center">
        <Text size="body-sm-desktop" tabularNums>
          {firstRow}–{lastRow} of {totalRows.toLocaleString("en")} rows
        </Text>
      </Stack>
      <Stack direction="row" align="start" gap={5} wrap>
        <Select.Root width="auto">
          <Stack direction="row" align="center" gap={2}>
            <Select.Label marginBottom={0} wrap="nowrap">
              <Text as="span" size="body-sm-desktop" weight="regular">
                Rows per page
              </Text>
            </Select.Label>
            <Stack width="72px">
              <Select.Field value={String(rowsPerPage)}>
                {[10, 25, 50].map((size) => (
                  <Select.Item
                    key={size}
                    selected={size === rowsPerPage}
                    onClick={() => {
                      setRowsPerPage(size);
                      setPage(1);
                    }}
                  >
                    {size}
                  </Select.Item>
                ))}
              </Select.Field>
            </Stack>
          </Stack>
        </Select.Root>
        <Pagination
          ariaLabel="Pagination: large dataset"
          previousAriaLabel="Previous page"
          nextAriaLabel="Next page"
          variant="compact"
          pageLabel="Page"
          totalLabel={({ total }) => `of ${total}`}
          pageInput={{
            ariaLabel: "Page number",
            errorMessage: ({ total }) => `Enter a whole number from 1 to ${total}.`,
          }}
          total={Math.ceil(totalRows / rowsPerPage)}
          page={page}
          onChange={({ page }) => setPage(page)}
        />
      </Stack>
    </Stack>
  );
};

Accessibility

Key
Description
TabShiftTab
Move between enabled controls and the optional page input.
EnterSpace
Activate a focused page, previous, or next button.
Enter
Follow a focused page link in link mode.
Enter
Submit the typed page number without submitting a surrounding form.
Provide ariaLabel to identify the pagination, such as ‘Search results pages’.
Provide previousLabel and nextLabel, or previousAriaLabel and nextAriaLabel for icon-only arrows.
Provide pageInput.ariaLabel and pageInput.errorMessage when enabling page input.