Page navigation for a paged collection — a row of square page cells with prev/next chevrons, or a compact "Page X of Y" readout. Truncated ranges collapse behind an ellipsis that opens a menu of the hidden pages.
Registry-only
No headless primitive — this component ships only as a copied styled file, so there is no Headless mode. primitiv add pagination installs it whichever mode you are reading.
Playground
Preview
import{ usePagination }from"@primitiv-ui/react";import{Pagination,PaginationList,PaginationItem,PaginationLink,PaginationPrevious,PaginationNext,PaginationEllipsis,PaginationMenuItem,}from"@/components/ui/pagination";const{ page, items, setPage, next, previous, canPrevious, canNext }=usePagination({ totalItems, pageSize:10});<Paginationsize="md"label="Results"><PaginationList><PaginationItem><PaginationPrevious disabled={!canPrevious} onClick={previous} /></PaginationItem>{items.map((item)=> item.type==="page"?(<PaginationItemkey={item.page}><PaginationLinkisActive={item.page=== page}onClick={()=>setPage(item.page)}>{item.page}</PaginationLink></PaginationItem>):(/* an ellipsis menu of the hidden pages */))}<PaginationItem><PaginationNext disabled={!canNext} onClick={next} /></PaginationItem></PaginationList></Pagination>
Density is set by a data-density ancestor — the Context system, not a Pagination prop.
No headless primitive — this one ships only as a copied styled surface, so primitiv add is the only way in, whichever mode you are reading.
Anatomy
Pagination is the <nav> landmark (name it with label, and give it variant / size). Inside, a PaginationList (<ul>) holds PaginationItem (<li>) cells: PaginationPrevious / PaginationNext chevrons, PaginationLinks for the pages (isActive sets aria-current), and a PaginationEllipsis whose children are PaginationMenuItems — the dropdown of collapsed pages. The compact variant swaps the number cells for a PaginationStatus readout. PaginationSummary (before the list) and PaginationTrailing (after) are optional slots for a “showing X–Y” line and a jump-to control. The page state itself is the usePagination hook's, not the markup's.
Generated from the copied file’s props type and contract.json. Never hand-maintained.
Pagination
Extends HTMLElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
label
string
"Pagination"
Accessible name for the navigation landmark. Give each Pagination on a
page a distinct one — two identically named landmarks are
indistinguishable in a landmark list.
size
"xs" | "sm" | "md" | "lg" | "xl"
md
Control size; data-density scales each size further.
variant
"numbered" | "compact"
numbered
Numbered page cells, or a compact Page X of Y readout for narrow containers.
PaginationSummary
Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.
No props of its own.
PaginationList
Extends HTMLUListElement — every native attribute of that element is accepted and forwarded.
No props of its own.
PaginationItem
Extends HTMLLIElement — every native attribute of that element is accepted and forwarded.
No props of its own.
PaginationLink
Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
asChild
boolean
false
Renders the child element instead of a native <button>, merging all
props — aria-, data-, event handlers, ref — onto it via
Slot. type is not forwarded in this mode; the child owns its
own type semantics. See the asChild example on Button.
children
ReactNode
—
Button content. Under asChild, becomes the single child element Slot merges props onto.
isActive
boolean
false
Marks this cell as the current page: renders primary rather than
secondary, and sets aria-current="page".
ref
Ref<HTMLButtonElement>
—
Allows getting a ref to the component instance.
Once the component unmounts, React will set ref.current to null
(or call the ref with null if you passed a callback ref).
Forwarded to the underlying HTMLButtonElement, or — under
asChild — merged onto the rendered child via Slot.
type
"button" | "submit" | "reset"
"button"
The button's native type attribute, restricted to the three valid
values. Defaults to "button" (not the DOM's own default of
"submit"), so a Button placed inside a <form> never triggers an
accidental submit unless set explicitly.
PaginationPrevious
Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
asChild
boolean
false
Renders the child element instead of a native <button>, merging all
props — aria-, data-, event handlers, ref — onto it via
Slot. type is not forwarded in this mode; the child owns its
own type semantics. See the asChild example on Button.
label
string
"Go to previous page"
ref
Ref<HTMLButtonElement>
—
Allows getting a ref to the component instance.
Once the component unmounts, React will set ref.current to null
(or call the ref with null if you passed a callback ref).
Forwarded to the underlying HTMLButtonElement, or — under
asChild — merged onto the rendered child via Slot.
type
"button" | "submit" | "reset"
"button"
The button's native type attribute, restricted to the three valid
values. Defaults to "button" (not the DOM's own default of
"submit"), so a Button placed inside a <form> never triggers an
accidental submit unless set explicitly.
PaginationNext
Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
asChild
boolean
false
Renders the child element instead of a native <button>, merging all
props — aria-, data-, event handlers, ref — onto it via
Slot. type is not forwarded in this mode; the child owns its
own type semantics. See the asChild example on Button.
label
string
"Go to previous page"
ref
Ref<HTMLButtonElement>
—
Allows getting a ref to the component instance.
Once the component unmounts, React will set ref.current to null
(or call the ref with null if you passed a callback ref).
Forwarded to the underlying HTMLButtonElement, or — under
asChild — merged onto the rendered child via Slot.
type
"button" | "submit" | "reset"
"button"
The button's native type attribute, restricted to the three valid
values. Defaults to "button" (not the DOM's own default of
"submit"), so a Button placed inside a <form> never triggers an
accidental submit unless set explicitly.
PaginationEllipsis
Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
asChild
boolean
false
Renders the child element instead of a native <button>, merging all
props — aria-, data-, event handlers, ref — onto it via
Slot. type is not forwarded in this mode; the child owns its
own type semantics. See the asChild example on Button.
children
ReactNode
—
The hidden pages, as PaginationMenuItem children.
label
string
"Show more pages"
ref
Ref<HTMLButtonElement>
—
Allows getting a ref to the component instance.
Once the component unmounts, React will set ref.current to null
(or call the ref with null if you passed a callback ref).
Forwarded to the underlying HTMLButtonElement, or — under
asChild — merged onto the rendered child via Slot.
type
"button" | "submit" | "reset"
"button"
The button's native type attribute, restricted to the three valid
values. Defaults to "button" (not the DOM's own default of
"submit"), so a Button placed inside a <form> never triggers an
accidental submit unless set explicitly.
PaginationMenuItem
Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
asChild
boolean
false
Render the composed child element (with menuitem semantics) instead of
the default <li role="menuitem">.
children
ReactNode
—
The item's visible content (or an element, with asChild).
disabled
boolean
false
Mark the item non-interactive. Sets aria-disabled="true" and skips
the item during arrow navigation, typeahead, and activation.
onSelect
(event: Event) => void
—
Fires when the item is activated (click, Enter, or Space). Called
with a cancellable event whose preventDefault() skips the auto-close
that Dropdown performs after selection.
ref
Ref<HTMLLIElement>
—
Allows getting a ref to the component instance.
Once the component unmounts, React will set ref.current to null
(or call the ref with null if you passed a callback ref).
Forwarded to the underlying HTMLLIElement.
PaginationStatus
Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.
No props of its own.
PaginationTrailing
Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.
No props of its own.
Styling contract
11 CSS custom properties on .primitiv-pagination — mode-agnostic. These names are the stable surface; the values are not.
It is a <nav> landmark — name it.Pagination renders a <nav> and needs a label (aria-label), since a page can have more than one navigation landmark; “Search results” or “Table pages” distinguishes them in a screen reader's landmark list.
The current page is marked with aria-current, not just colour.isActive on a PaginationLink sets aria-current="page", which is how assistive technology announces which page you are on — the highlight alone is invisible to it.
Prev/next carry their own names and disabled state.PaginationPrevious / PaginationNext are icon-only, so they set an aria-label (Go to previous page) themselves; wire disabled from canPrevious / canNext so the first/last page can't be over-stepped.
The ellipsis is a real menu, not decoration.PaginationEllipsis opens a dropdown of the collapsed pages (PaginationMenuItems), so every page stays reachable by keyboard — a bare “…” glyph would strand the hidden pages.
Links or buttons? These are button-based controls driving client state, not <a href>s — right for a SPA. If each page is a real URL, render PaginationLinkasChild around your router's link so they are genuine, right-clickable links.
The state is the hook's, the arithmetic is too.usePagination clamps out-of-range requests and computes the truncated range, so you don't hand-roll off-by-one page maths that a keyboard or screen-reader user then trips over.
Examples
Every example below reacts to the density control. Currently showing Styled mode.
Numbered pages
The default: a row of page cells between prev/next chevrons, driven by usePagination. The hook returns items — a mix of page and gap entries — so a long range collapses behind a PaginationEllipsis whose menu lists the hidden pages (this demo is 10 pages; click the …). isActive on the current PaginationLink is what sets aria-current="page"; disable the chevrons from canPrevious / canNext.
variant="compact" drops the number cells for a PaginationStatus readout between the chevrons — for narrow containers, and the intended default in a data-table footer. Same hook, less chrome: you show Page {page} of {pageCount} instead of every cell.
Two optional slots wrap the list: PaginationSummary before it for a “showing X–Y of N” line (from the hook's startIndex / endIndex), and PaginationTrailing after it for a jump-to control — a native Select here, so far pages are one action away rather than many clicks. Omit either and nothing renders.
import{ usePagination }from"@primitiv-ui/react";import{Pagination,PaginationList,PaginationItem,PaginationLink,PaginationPrevious,PaginationNext,PaginationEllipsis,PaginationMenuItem,}from"@/components/ui/pagination";import{Select,SelectItem}from"@/components/ui/select";<Paginationlabel="Results"><PaginationSummary>Showing {startIndex +1}–{endIndex} of {total}</PaginationSummary><PaginationList>{/* … prev / pages / next … */}</PaginationList><PaginationTrailing><span>Go to</span><Selectnativevalue={String(page)}onValueChange={(v)=>setPage(Number(v))}aria-label="Go to page">{pages.map((n)=><SelectItemkey={n}value={String(n)}>{n}</SelectItem>)}</Select></PaginationTrailing></Pagination>
Sizes and density
Five sizes, each rescaling again with the nearest data-density ancestor — the same framed-control/* scale the chevrons and cells share with Button and Input, so a pagination row lines up with the controls around it.