Pagination

Pagination with page navigation, next and previous links.

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

export default function PaginationDemo() {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="#" />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#" isActive>
            2
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">3</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationEllipsis />
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href="#" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

Installation

Install the following dependencies:

pnpm add @base-ui/react @stylexjs/stylex clsx lucide-react

Set up the token layer and the StyleX compiler, if you have not already. See Installation. Pagination also needs the Button, which it imports buttonStyles from, and interop.css, which sizes the chevrons and the ellipsis icon.

Copy and paste the following code into your project.

components/ui/pagination.tsx
"use client"

import { useRender } from "@base-ui/react/use-render"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"
import { clsx } from "clsx"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"

import { buttonStyles, type ButtonSize } from "@/components/ui/button"
import { breakpoints } from "@/theme/breakpoints.stylex"
import { space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  nav: {
    display: "flex",
    justifyContent: "center",
    width: "100%",
    marginInline: "auto",
  },
  content: {
    display: "flex",
    alignItems: "center",
    gap: space.s0_5,
    listStyleType: "none",
  },
  ellipsis: {
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    width: space.s8,
    height: space.s8,
    "--carbon-icon-size": space.s4,
  },
  text: {
    display: { default: "none", [breakpoints.sm]: "block" },
  },
})

const paginationStyles = {
  nav: styles.nav,
  content: styles.content,
  ellipsis: styles.ellipsis,
  text: styles.text,
}

type Overrides = { xstyle?: StyleXStyles; className?: string }

function Pagination({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"nav"> & Overrides) {
  const sx = stylex.props(styles.nav, xstyle)
  return useRender({
    defaultTagName: "nav",
    render,
    ref,
    props: {
      "aria-label": "pagination",
      "data-slot": "pagination",
      ...sx,
      ...props,
      className: clsx(sx.className, className),
      style: { ...sx.style, ...style },
    },
  })
}

function PaginationContent({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"ul"> & Overrides) {
  const sx = stylex.props(styles.content, xstyle)
  return useRender({
    defaultTagName: "ul",
    render,
    ref,
    props: {
      "data-slot": "pagination-content",
      ...sx,
      ...props,
      className: clsx(sx.className, className),
      style: { ...sx.style, ...style },
    },
  })
}

function PaginationItem({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"li"> & Overrides) {
  const sx = stylex.props(xstyle)
  return useRender({
    defaultTagName: "li",
    render,
    ref,
    props: {
      "data-slot": "pagination-item",
      ...sx,
      ...props,
      className: clsx(sx.className, className),
      style: { ...sx.style, ...style },
    },
  })
}

type PaginationLinkProps = useRender.ComponentProps<"a", { active: boolean }> & {
  isActive?: boolean
  size?: ButtonSize
  xstyle?: StyleXStyles
}

function PaginationLink({
  isActive = false,
  size = "icon",
  xstyle,
  className,
  render,
  ...props
}: PaginationLinkProps) {
  const variant = isActive ? "outline" : "ghost"
  const sx = stylex.props(
    buttonStyles.base,
    buttonStyles.pressable,
    buttonStyles.variants[variant],
    buttonStyles.sizes[size],
    xstyle,
  )
  return useRender({
    defaultTagName: "a",
    render,
    state: { active: isActive },
    stateAttributesMapping: { active: (active) => (active ? { "data-active": "true" } : null) },
    props: {
      "aria-current": isActive ? "page" : undefined,
      "data-slot": "pagination-link",
      "data-variant": variant,
      "data-size": size,
      ...sx,
      ...props,
      className: clsx("carbon-icons", sx.className, className),
    },
  })
}

type PaginationDirectionProps = PaginationLinkProps & {
  text?: string
}

function PaginationPrevious({
  text = "Previous",
  size = "default",
  xstyle,
  ...props
}: PaginationDirectionProps) {
  return (
    <PaginationLink
      aria-label="Go to previous page"
      size={size}
      xstyle={[buttonStyles.iconStart[size], xstyle]}
      {...props}
    >
      <ChevronLeft />
      <span data-slot="pagination-previous-text" {...stylex.props(styles.text)}>
        {text}
      </span>
    </PaginationLink>
  )
}

function PaginationNext({
  text = "Next",
  size = "default",
  xstyle,
  ...props
}: PaginationDirectionProps) {
  return (
    <PaginationLink
      aria-label="Go to next page"
      size={size}
      xstyle={[buttonStyles.iconEnd[size], xstyle]}
      {...props}
    >
      <span data-slot="pagination-next-text" {...stylex.props(styles.text)}>
        {text}
      </span>
      <ChevronRight />
    </PaginationLink>
  )
}

function PaginationEllipsis({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"span"> & Overrides) {
  const sx = stylex.props(styles.ellipsis, xstyle)
  return useRender({
    defaultTagName: "span",
    render,
    ref,
    props: {
      "aria-hidden": "true",
      "data-slot": "pagination-ellipsis",
      ...sx,
      ...props,
      className: clsx("carbon-icons", sx.className, className),
      style: { ...sx.style, ...style },
      children: (
        <>
          <MoreHorizontal />
        </>
      ),
    },
  })
}

export {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
  paginationStyles,
}

Update the import paths to match your project setup.

Usage

import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"
<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>
        2
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">3</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" />
    </PaginationItem>
  </PaginationContent>
</Pagination>

Composition

Pagination
└── PaginationContent
    ├── PaginationItem
    │   └── PaginationPrevious
    ├── PaginationItem
    │   └── PaginationLink
    ├── PaginationItem
    │   └── PaginationEllipsis
    └── PaginationItem
        └── PaginationNext

Simple

Page numbers only. Pass isActive to the current page.

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

export default function PaginationSimple() {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationLink href="#">1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#" isActive>
            2
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">3</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">4</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">5</PaginationLink>
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

Icons only

Use PaginationPrevious and PaginationNext without page numbers, next to a rows-per-page control.

import * as stylex from "@stylexjs/stylex"
import { Field, FieldLabel } from "@/components/ui/field"
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { HStack } from "@/components/ui/stack"

const styles = stylex.create({
  row: { width: "100%" },
  field: { width: "fit-content" },
  trigger: { width: "5rem" },
  pagination: { width: "auto" },
})

const rows = ["10", "25", "50", "100"]

export default function PaginationIconsOnly() {
  return (
    <HStack gap={4} align="center" justify="between" xstyle={styles.row}>
      <Field orientation="horizontal" xstyle={styles.field}>
        <FieldLabel>Rows per page</FieldLabel>
        <Select defaultValue="25">
          <SelectTrigger xstyle={styles.trigger} id="select-rows-per-page">
            <SelectValue />
          </SelectTrigger>
          <SelectContent align="start">
            <SelectGroup>
              {rows.map((value) => (
                <SelectItem key={value} value={value}>
                  {value}
                </SelectItem>
              ))}
            </SelectGroup>
          </SelectContent>
        </Select>
      </Field>
      <Pagination xstyle={styles.pagination}>
        <PaginationContent>
          <PaginationItem>
            <PaginationPrevious href="#" />
          </PaginationItem>
          <PaginationItem>
            <PaginationNext href="#" />
          </PaginationItem>
        </PaginationContent>
      </Pagination>
    </HStack>
  )
}

Pagination fills the width of its container and centres the links. Beside another control, set width: "auto" through xstyle so the nav shrinks to its content.

Pass a router link through render on PaginationLink. PaginationLink then applies the button styles and the props to your component instead of an a.

import { Link } from "@tanstack/react-router"
<PaginationLink render={<Link to="/posts" search={{ page: 2 }} />} isActive>
  2
</PaginationLink>

Do not render a PaginationLink from Button. Base UI's Button puts role="button" on the element it renders, which takes the link role away. Middle click and open-in-new-tab stop working, and a screen reader announces a button. PaginationLink is an a styled with buttonStyles for this reason.

API reference

Pass render to replace a part's element or compose it with another component.

Every part takes xstyle and className.

Pagination

The nav landmark. It renders aria-label="pagination"; pass your own when a page has more than one.

Renders an a and accepts its props. The current page takes the outline button variant and aria-current="page"; every other page is ghost.

PropTypeDefault
isActivebooleanfalse
size"default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg""icon"
renderReact.ReactElement<a />

PaginationPrevious and PaginationNext

A PaginationLink with a chevron and a word. The word is hidden below 40rem, and the aria-label reads "Go to previous page" or "Go to next page" whatever text says.

PropTypeDefault
textstring"Previous" / "Next"
size"default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg""default"

PaginationEllipsis

Renders a span marked aria-hidden. It is the size of an icon link, so the row keeps its rhythm.

paginationStyles

The style map behind the component. Spread it onto any element. A link's styles are buttonStyles from the Button page.

KeyDescription
navThe landmark, full width, its links centred
contentThe list row and the gap between links
ellipsisThe ellipsis box and its icon size
textThe word beside a chevron, hidden below 40rem

Data attributes

AttributeOnEffect
data-slot="pagination"the navTargets the landmark from a parent
data-slot="pagination-content"the listTargets the row
data-slot="pagination-item"an itemTargets one list item
data-slot="pagination-link"a linkPresent on whatever render produces
data-activethe current linkSet when isActive is true
data-variant, data-sizea linkoutline or ghost, and the size prop
data-slot="pagination-previous-text"the wordThe text inside PaginationPrevious
data-slot="pagination-next-text"the wordThe text inside PaginationNext
data-slot="pagination-ellipsis"the ellipsisAlso aria-hidden

A link, the previous and next controls and the ellipsis carry the carbon-icons class and set --carbon-icon-size. interop.css reads both to size the icon inside each.