Pagination and Load more
Pagination and Load more split long lists, such as a course library, into numbered pages or batches that learners add as they go.
Beta until design adds pagination to the Figma design system. Nuton's Figma has none (audit C3), so the whole visual design is code-only, built from Button and segmented-control tokens. The live app uses Load more in the library.
Preview
Change the controls to try every supported combination. Make the window narrow to see the auto variant switch to its compact form.
Links: each page has its own URL (here a #fragment, so the docs page stays put).
import { Pagination } from '@nuton/ui'
<Pagination
page={page}
pageCount={12}
getHref={(p) => `/library?page=${p}`}
labels={{ nav: 'Library pages' }}
/>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages and aren't published yet (D4, D12). Inside the Nuton monorepo, add them as workspace dependencies.
pnpm add @nuton/ui @nuton/tokensnpm install @nuton/ui @nuton/tokensyarn add @nuton/ui @nuton/tokensimport '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'Usage
import { LoadMore, Pagination } from '@nuton/ui'
// Each page has its own URL: links
<Pagination page={page} pageCount={12} getHref={(p) => `/library?page=${p}`} labels={{ nav: 'Library pages' }} />
// The list changes in place: buttons
<Pagination page={page} pageCount={12} onPageChange={setPage} />
// Learners add the next batch at the end of the list
<LoadMore shown={courses.length} total={120} loading={isFetching} onLoadMore={fetchNextPage} />- Load more suits browsing: a library learners scroll through. It is what the live library uses. Pagination suits finding things again: search results, long tables, anything people share or return to.
- Prefer links (
getHref) when pages have URLs: they work without JavaScript and can open in a new tab. Pass a router's link withlinkAs. - Name the landmark after what it pages (“Library pages”), especially when a page has more than one navigation.
- Don't load more automatically on scroll alone. Keyboard and screen-reader users can never reach what comes after the list.
Paginationhas no hooks, so it renders in server components.LoadMoreis a client component.
Variants and sizes
One size. Pagination has three layouts; LoadMore has a count and a button.
Numbered
Previous, page numbers with gaps, Next. The number of slots stays the same as the page changes, so the arrows don't jump.
<Pagination page={6} pageCount={12} getHref={href} variant="numbers" />Compact
“Page 3 of 12” between the arrows, for phones and narrow columns. The default auto variant switches to it below 30rem of its container's width.
<Pagination page={3} pageCount={12} getHref={href} variant="compact" showFirstLast />First and last
<Pagination page={20} pageCount={40} getHref={href} variant="numbers" showFirstLast siblingCount={2} />| Part | Measurement (code-only) |
|---|---|
| Page and arrow controls | 32 × 32 minimum, pill, gap 4; numbers widen for 3 digits |
| Type | Label/Small, tabular numbers; arrows are 20px Remix icons |
| Compact text | Label/Small, Text/Sub-600, 8px each side |
| LoadMore | Count (Paragraph/Small, Sub-600), 12 gap, Button stroke sm (36 tall) |
States and interactions
aria-current="page".href; a button stays focusable with aria-disabled.Showing 24 of 120
State tokens
Set on .nu-pagination.
| Token | Value | Used for |
|---|---|---|
--nu-pagination-size | 32px | Control size |
--nu-pagination-fg · -fg-hover · -bg-hover | Text/Sub-600 · Text/Strong-950 · Bg/Weak-50 | Resting and hover |
--nu-pagination-bg-current · -fg-current | Action primary (Bg/Strong-950 · Text/White) | Current page |
--nu-pagination-fg-disabled | Icon/Disabled-300 | Unavailable arrows |
Composition examples
A paged library
Button mode: the list changes in place. Focus stays on the control you used, and the compact text is a polite live region so the new page is announced.
Load more courses
The live library pattern. New courses are added after the last one; the count updates and “12 more courses loaded” is announced. When everything has loaded, the button goes and focus moves to the count.
API reference
Pagination props
| Prop | Type | Default | Description |
|---|---|---|---|
page | number | — | Current page, 1-based. Clamped to 1…pageCount. |
pageCount | number | — | Total pages. Nothing renders for fewer than 2. |
getHref | (page) => string | — | Link mode: every control is a link to this URL. |
linkAs | ElementType | — | A router's link component for link mode. Receives href, className and ARIA props. |
onPageChange | (page) => void | — | Button mode (or an extra click handler in link mode). Not called for the current page or unavailable arrows. |
variant | 'auto' | 'numbers' | 'compact' | 'auto' | auto switches from numbers to compact below 30rem of container width. |
siblingCount | number | 1 | Pages on each side of the current one. |
boundaryCount | number | 1 | Pages always shown at each end. |
showFirstLast | boolean | false | Adds First page and Last page arrows. |
align | 'start' | 'center' | 'end' | 'start' | Where the controls sit. The nav always fills its container (it is a size container for auto). |
labels | Partial<PaginationLabels> | — | nav ("Pagination"), previous ("Previous page"), next, first, last, page(n) ("Page 3"), pageOf(n, count) ("Page 3 of 12"). |
…nav props | HTMLAttributes<HTMLElement> | — | className, id and data attributes go to the nav. The ref points at it. |
LoadMore props
| Prop | Type | Default | Description |
|---|---|---|---|
shown | number | — | Items on screen now. An increase is announced. |
total | number | — | All items, when known: "Showing 24 of 120". |
hasMore | boolean | — | Whether more can load. Default: shown < total, or true when total is unknown. |
loading | boolean | false | Spinner, "Loading more…" and aria-busy; presses are ignored. |
onLoadMore | () => void | — | Load and append the next batch, then update shown. |
hideCount | boolean | false | Hides the count while more can load. It is still announced. |
size · appearance | ButtonSize · ButtonAppearance | 'sm' · 'stroke' | Passed to the Button. |
fullWidth | boolean | false | Stretches the button, for phones. |
labels | Partial<LoadMoreLabels> | — | button ("Load more"), loading ("Loading more…"), count(shown, total), loaded(added, shown, total), done(shown). |
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves through the arrows and page numbers, or to the Load more button |
| Enter | Follows a page link or presses a page button |
| Space | Presses a page button or Load more |
Screen readers
A nav landmark named “Pagination” (override it) with a list. Controls are named “Page 3”, “Next page”; the current one has aria-current="page". Gaps are hidden. LoadMore announces “24 more loaded. Showing 48 of 120.” in a polite status region.
Focus
Unavailable buttons stay focusable, so focus isn't lost on the last page. Focus stays on Load more after each batch and moves to the count when the button goes. Ring: the focus-ring tokens.
Contrast and colour
Numbers Sub-600 on white 6.69:1; current page white on Strong-950 17.93:1. The current page differs by fill as well as colour. Forced colours use Highlight for the current page.
Touch and pointer
Every control is at least 32 × 32 (WCAG 2.5.8 asks for 24). The Load more button is 36 tall, or full width with fullWidth.
You must: name the landmark when there is more than one, keep the list's order stable as items are added, and never load more on scroll alone.
Not in Figma
Nuton's Figma file has no pagination or load-more design (audit C3). Everything visual here is code-only and listed in the CSS spec comment for design review.
Guidelines and related components
Use Pagination for
- Search results and long tables
- Lists people share or come back to at a given page
Use Load more for
- Browsing a library or feed
- Lists where the order matters more than the position
Do and don't
Showing 24 of 120
Scroll to load more…
‹ 1 2 3 ›