Skip to content

Pagination

beta

Previous, next, and numbered page links in a labeled nav, with the current page marked and an ellipsis for gaps.

Preview

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @arbokui in a public list that Arbokui does not control.

    npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @arbokui/theme
  3. 3. Add the component.

    npx shadcn@latest add @arbokui/pagination

Use it for

  • Moving through a long list split into pages, like search results, orders, or a table.
  • Mark the page people are on with current. It sets aria-current="page" and draws an outline.
  • Keep the first and last page, the pages beside the current one, and a PaginationEllipsis for each gap.
  • Set disabled on Previous on the first page and on Next on the last, so the row keeps its shape.
  • Put the page in the URL, like ?page=3, so people can share it and the back button works.
  • With a router, pass its link as render: <PaginationLink render={<Link href="?page=2" />}>2</PaginationLink>.
  • Set the same size on every link and the ellipsis in one row.

Avoid it for

  • Short lists that fit on one screen. Show them all.
  • Feeds people scroll through without a goal. A Load more button keeps their place better.
  • Steps of a form or a flow. Use StepDots, or Back and Next buttons.
  • Showing every page of a long list. Phones wrap the row. Show about seven items at most.
  • Two navs named "Pagination" on one page, like above and below a table. Pass a different aria-label to each.

Props

  • size

    On PaginationLink, PaginationPrevious, PaginationNext, and PaginationEllipsis. Height and width. Density scales them.

    "sm" | "md" | "lg" · default "md"

  • current

    On PaginationLink. The page people are on. Sets aria-current="page" and an outline.

    boolean · default false

  • disabled

    On PaginationLink, PaginationPrevious, and PaginationNext. Drops href and onClick, sets aria-disabled, and dims the link. It is not a tab stop.

    boolean · default false

  • aria-label

    On Pagination. The landmark's name. Rename it when a page has more than one. Left off when you pass aria-labelledby.

    string · default "Pagination"

  • label

    On PaginationEllipsis. What screen readers hear in place of the dots.

    string · default "More pages"

  • render

    On PaginationLink, PaginationPrevious, and PaginationNext. Renders your element, like a router link, with the link's props and classes. A disabled link ignores it and renders an <a>.

    ReactElement

  • Pagination

    The landmark. A <nav> with aria-label="Pagination". Pass aria-label or aria-labelledby to rename it.

    Omit<ComponentProps<"nav">, "color">

  • PaginationContent

    Part. The list of items. It wraps as a last resort, so it never pushes the page sideways.

    Omit<ComponentProps<"ul">, "color">

  • PaginationItem

    Part. One item in the row.

    Omit<ComponentProps<"li">, "color">

  • PaginationLink

    Part. One page, styled with buttonVariants: ghost, or outline when current. At least as wide as it is tall.

    Omit<ComponentProps<"a">, "color"> & { current?, disabled?, size?, render? }

  • PaginationPrevious

    Part. Named "Previous page". Below the sm breakpoint the words are visually hidden and the link turns square.

    Omit<ComponentProps<"a">, "color"> & { disabled?, size?, render? }

  • PaginationNext

    Part. Named "Next page". Below the sm breakpoint the words are visually hidden and the link turns square.

    Omit<ComponentProps<"a">, "color"> & { disabled?, size?, render? }

  • PaginationEllipsis

    Part. Three dots for pages left out. label is what screen readers hear. Default "More pages".

    Omit<ComponentProps<"span">, "children" | "color"> & { label?, size? }

  • className

    Extra classes on any part. Later classes win, so className='rounded-full' on PaginationLink replaces the radius.

    string

Accessibility

  • Pagination renders a <nav> named "Pagination", so screen readers list it with the page's landmarks.
  • The items are a <ul>, so screen readers say how many there are.
  • current sets aria-current="page". The current page also gets an outline, so it is marked by shape, not only by color.
  • Previous and Next are named "Previous page" and "Next page". On narrow screens the words are visually hidden but still read. Their chevrons are aria-hidden.
  • A disabled link has no href, so it is not a tab stop. It keeps role="link" and aria-disabled="true", so screen readers still say it is there and unavailable.
  • PaginationEllipsis has visually hidden text, "More pages" by default. Its dots are aria-hidden, and it is not interactive.
  • Links are native <a> elements, so Tab moves between them and Enter follows them.
  • Link text uses the tone's soft foreground, held to 4.5:1 on the page. The current page's outline is held to 3:1, and so is the focus ring.

Examples

Page 1 of 10

<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious disabled />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="?page=1" current>
        1
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="?page=2">2</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="?page=3">3</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="?page=10">10</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="?page=2" />
    </PaginationItem>
  </PaginationContent>
</Pagination>

With Next.js links

import Link from "next/link";

<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious render={<Link href="?page=4" />} />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink render={<Link href="?page=5" />} current>
        5
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationNext render={<Link href="?page=6" />} />
    </PaginationItem>
  </PaginationContent>
</Pagination>

A row built from the current page

import { Fragment } from "react";

function Pages({ page, total }: { page: number; total: number }) {
  const shown = [...new Set([1, page - 1, page, page + 1, total])]
    .filter((n) => n >= 1 && n <= total)
    .sort((a, b) => a - b);
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href={`?page=${page - 1}`} disabled={page === 1} />
        </PaginationItem>
        {shown.map((n, i) => (
          <Fragment key={n}>
            {i > 0 && n - shown[i - 1] > 1 ? (
              <PaginationItem>
                <PaginationEllipsis />
              </PaginationItem>
            ) : null}
            <PaginationItem>
              <PaginationLink href={`?page=${n}`} current={n === page}>
                {n}
              </PaginationLink>
            </PaginationItem>
          </Fragment>
        ))}
        <PaginationItem>
          <PaginationNext href={`?page=${page + 1}`} disabled={page === total} />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  );
}

Above and below a table

<Pagination aria-label="Pages, top">
  {/* ... */}
</Pagination>
{/* the table */}
<Pagination aria-label="Pages, bottom">
  {/* ... */}
</Pagination>