Pagination

A row of page links for moving through a long list or a set of results. The page links, Previous and Next are all Buttons, in three variants, two shapes and four sizes, the same as the Figma set.

Installation

bash
npx @smicolon/fasla-ui@latest add pagination

Run init once per project before adding components. See Installation.

Pagination is built on Button, so adding it adds button too.

Preview

Variants

variant picks the Button type of each part. default has ghost page links and a solid current page. solid has soft page links and a solid current page. outlined has outline page links, a soft current page, and outline Previous and Next. In the other two, Previous and Next are ghost.

Sizes

The four Button sizes: xs is 26px tall, sm 32px, md 36px and lg 40px. A page link is a square of that height, and its number is 14px at every size. A number too wide for its square, such as 100 at xs, widens it.

Shape

shape="square" gives every part the Button's standard 8px corners, and shape="rounded" its full radius: circles for the page links, pills for Previous and Next. The focus ring follows the shape.

Previous and Next

PaginationPrevious and PaginationNext show a label and a chevron. The label follows the page's language, Previous and Next or السابق and التالي; pass text to change it. With iconOnly they show the chevron alone in a square, and keep the label for screen readers. Disable Previous on the first page and Next on the last with disabled.

Long ranges

Show the first and last pages, the current page with one on each side, and put PaginationEllipsis where pages are left out: 1 … 5 6 7 … 20. The ellipsis is a square the size of a page link, but not a link, and screen readers hear “More pages”. Icon-only Previous and Next keep a long row short. On a phone, show fewer pages around the current one, as this example does.

Accessibility

  • Pagination is a nav landmark. Name it in the page's language with aria-label, as the examples here do; otherwise it is called Pagination. Give each one on a page its own name.
  • The current page carries aria-current="page", so screen readers announce it as the current page.
  • Previous and Next are named by their label, which stays readable when iconOnly hides it. The chevrons are hidden from assistive technology.
  • The ellipsis takes no focus. Its glyph is hidden, and screen readers hear “More pages”, or “مزيد من الصفحات” when the nearest lang is Arabic.
  • A disabled link carries aria-disabled, leaves the tab order and ignores clicks. It stays a link, so screen readers still find it.
  • Every part is a Button, so keyboard focus draws the Button's 2px ring, 2px outside the link.
  • Direction follows the page's dir; there is no prop. Right to left, the row starts on the right with Previous, and each chevron turns to point outward. Page numbers stay in Western digits.

Props

Pagination

PropTypeDefaultDescription
variant"default" | "solid" | "outlined""default"The Button type of each part: ghost, soft or outline page links.
shape"square" | "rounded""square"The corners of every part: the Button's standard radius, or its full radius.
size"xs" | "sm" | "md" | "lg""md"The height of every part, from 26px to 40px.
aria-labelstring"Pagination"The landmark's name for screen readers, in the page's language.
childrenReactNode—A PaginationContent.
classNamestring—Extra classes for the nav element, a centred row by default.

PaginationContent

PropTypeDefaultDescription
childrenReactNode—The PaginationItems, in reading order.
classNamestring—Extra classes for the ul element. It wraps onto a second row rather than overflow.

PaginationItem

PropTypeDefaultDescription
childrenReactNode—One page link, Previous, Next or the ellipsis.
classNamestring—Extra classes for the li element.

PaginationLink

PropTypeDefaultDescription
hrefstring—Where the link goes. The link takes every other attribute of an a element too.
isActivebooleanfalseMarks the current page: the current-page look and aria-current="page".
disabledbooleanfalseDims the link, takes it out of the tab order and ignores clicks.
asChildbooleanfalseMakes its only child the link, with these styles: Next.js's Link, say, or a button when paging changes state rather than the URL.
childrenReactNode—The page number, in Western digits on Arabic pages too.
classNamestring—Extra classes for the link.

PaginationPrevious, PaginationNext

PropTypeDefaultDescription
hrefstring—Where the link goes.
textReactNode—The label. Previous or Next by default, in the page's language.
iconOnlybooleanfalseShows the chevron alone, in a square. The label stays for screen readers.
disabledbooleanfalseDims the link, takes it out of the tab order and ignores clicks.
asChildbooleanfalseMakes its only child the link, and puts the label and chevron inside it.
classNamestring—Extra classes for the link.

PaginationEllipsis

PropTypeDefaultDescription
classNamestring—Extra classes for the ellipsis. Its screen-reader text is built in.

Usage

Links to pages

tsx
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

export function OrdersPagination() {
  return (
    <Pagination aria-label="Pagination">
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="?page=1" />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="?page=1">1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="?page=2" isActive>
            2
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="?page=3">3</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href="?page=3" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

A long range

tsx
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

const PAGES = [1, null, 5, 6, 7, null, 20]

export function ArticlesPagination() {
  return (
    <Pagination aria-label="Pagination" variant="outlined">
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="?page=5" iconOnly />
        </PaginationItem>
        {PAGES.map((page, i) => (
          <PaginationItem key={i}>
            {page === null ? (
              <PaginationEllipsis />
            ) : (
              <PaginationLink href={`?page=${page}`} isActive={page === 6}>
                {page}
              </PaginationLink>
            )}
          </PaginationItem>
        ))}
        <PaginationItem>
          <PaginationNext href="?page=7" iconOnly />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

Paging with state

tsx
"use client"

import { useState } from "react"
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

const TOTAL = 5

export function TablePagination() {
  const [page, setPage] = useState(1)

  return (
    <Pagination aria-label="Pagination" size="sm">
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious asChild disabled={page === 1}>
            <button type="button" onClick={() => setPage(page - 1)} />
          </PaginationPrevious>
        </PaginationItem>
        {Array.from({ length: TOTAL }, (_, i) => i + 1).map((n) => (
          <PaginationItem key={n}>
            <PaginationLink asChild isActive={n === page}>
              <button type="button" onClick={() => setPage(n)}>
                {n}
              </button>
            </PaginationLink>
          </PaginationItem>
        ))}
        <PaginationItem>
          <PaginationNext asChild disabled={page === TOTAL}>
            <button type="button" onClick={() => setPage(page + 1)} />
          </PaginationNext>
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

With the Next.js Link

tsx
import Link from "next/link"
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
} from "@/components/ui/pagination"

export function ResultsPagination() {
  return (
    <Pagination aria-label="Pagination" shape="rounded">
      <PaginationContent>
        <PaginationItem>
          <PaginationLink asChild isActive>
            <Link href="/results?page=1">1</Link>
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink asChild>
            <Link href="/results?page=2">2</Link>
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationNext asChild>
            <Link href="/results?page=2" />
          </PaginationNext>
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}