Pagination
Moving through long paged lists: Previous and Next around the page numbers, or around a "Page 5 of 12" count.
import { Pagination } from '@fairgarden-private/design/components/Pagination'
Pass the current page, the page count and getHref, which gives every page its own URL; renderLink builds each anchor, for example (href) => <NextLink href={href} />. Previous and Next are text Buttons rendered as anchors, in type-label caps with the chevron_left and chevron_right glyphs at the inline tier. Page numbers are Link kind="nav" in mono at label size with tabular figures, so a changing count never shifts the row. The current page is unlinked, bold and barred. On the first and last pages the Previous or Next control is omitted and its space kept, never shown disabled. While the list loads, mark the list region aria-busy; the pager stays put.
Numbered, compact, step, dots and more
Numbered, compact, step, dots and more
numbered (the default) reflows on its own width: below 768 px it shows the compact count; from 768 px the page numbers with one sibling on each side of the current page; from 1024 px two. compact keeps the count at every width, for lists over 50 pages. Below 360 px the words drop and the chevrons stand alone, still named "Previous" and "Next". The root is an inline-size container, so give it a width in shrink-to-fit layouts. Drag the frame's corner to watch the reflow.
step is the pager for carousels and figures: icon-only ‹ › circles around a mono "3 of 12". Pass onPageChange to handle the buttons, getHref to make them links, or both. dots adds up to 8 position dots under the same count, the current one solid and larger; with more than 8 it is the plain step pager. Neither prints. more is for feeds: "Showing 20 of 54" over an outline "Show More" Button that links to the next page, so every step has a URL; pass items for the count. Page numbers and the Previous and Next words take the bare-text underline on hover, and the chevrons their heavier weight.
numbered
compact
step
dots
more
'use client'
import * as React from 'react'
import { Pagination } from '@fairgarden-private/design/components/Pagination'
import styles from './forms.module.css'
/** Keeps the demo on this page: a click on a page link sets the page instead of navigating. */
function usePager(initial: number) {
const [page, setPage] = React.useState(initial)
const onClick = (event: React.MouseEvent<HTMLElement>) => {
const anchor = (event.target as HTMLElement).closest('a')
const match = anchor?.hash.match(/^#page-(\d+)$/)
if (match) {
event.preventDefault()
setPage(Number(match[1]))
}
}
return { page, onClick }
}
const href = (page: number) => `#page-${page}`
export function PaginationForms() {
const numbered = usePager(5)
const compact = usePager(5)
const more = usePager(1)
const [step, setStep] = React.useState(3)
const [dots, setDots] = React.useState(2)
return (
<div className={styles.stack}>
<p className={styles.name}>numbered</p>
<div className={styles.frame}>
<Pagination
page={numbered.page}
count={12}
getHref={href}
onClick={numbered.onClick}
items={{
first: (numbered.page - 1) * 10 + 1,
last: Math.min(numbered.page * 10, 118),
total: 118,
}}
/>
</div>
<p className={styles.name}>compact</p>
<div className={styles.frame}>
<Pagination
kind="compact"
page={compact.page}
count={60}
getHref={href}
onClick={compact.onClick}
/>
</div>
<p className={styles.name}>step</p>
<div className={styles.frame}>
<Pagination kind="step" page={step} count={12} onPageChange={setStep} />
</div>
<p className={styles.name}>dots</p>
<div className={styles.frame}>
<Pagination
kind="dots"
page={dots}
count={6}
onPageChange={setDots}
previousLabel="Previous photo"
nextLabel="Next photo"
/>
</div>
<p className={styles.name}>more</p>
<div className={styles.frame}>
<Pagination
kind="more"
page={more.page}
count={3}
getHref={href}
onClick={more.onClick}
items={{ first: 1, last: Math.min(more.page * 20, 54), total: 54 }}
/>
</div>
</div>
)
}
On grounds
On page grounds and a deep field
The chevrons take the glyph role: the accent on a page ground, the text ink on a deep field. Lists belong on reading grounds, so saturated fields are rare.
paper
tide
forest
'use client'
import * as React from 'react'
import { Ground } from '@fairgarden-private/design/components/Ground'
import { Pagination } from '@fairgarden-private/design/components/Pagination'
import {
isFieldPreset,
isPageGroundPreset,
presets,
type GroundPreset,
} from '@fairgarden-private/design/utils/scope'
import styles from './grounds.module.css'
/** The first preset that passes `test`, read from the preset table, never named here. */
function firstPreset(test: (preset: GroundPreset) => boolean): GroundPreset | undefined {
return (Object.keys(presets) as GroundPreset[]).find(test)
}
/** A sample surface: a field preset as an inset field, a page ground as a face. */
function Sample({
preset,
className,
children,
}: {
preset: GroundPreset
className: string
children: React.ReactNode
}) {
if (isFieldPreset(preset)) {
return (
<Ground kind="field" preset={preset} className={className}>
{children}
</Ground>
)
}
if (isPageGroundPreset(preset)) {
return (
<Ground kind="face" preset={preset} className={className}>
{children}
</Ground>
)
}
return null
}
/** A light page ground, a pastel and a deep field; lists rarely sit on saturated fields. */
const grounds = [
firstPreset((preset) => presets[preset].tone === 'light-base'),
firstPreset((preset) => presets[preset].tone === 'tinted'),
firstPreset((preset) => isFieldPreset(preset) && presets[preset].mode === 'always-dark'),
].filter((preset): preset is GroundPreset => preset != null)
const href = (page: number) => `#grounds-${page}`
export function PaginationGrounds() {
return (
<div className={styles.stack}>
{grounds.map((preset) => (
<Sample key={preset} preset={preset} className={styles.face}>
<p className={styles.name}>{preset}</p>
<Pagination kind="compact" page={3} count={12} getHref={href} />
</Sample>
))}
</div>
)
}
In print the pager is hidden and its links print nothing. Pass items to print one state line in its place, "Items 41–50 of 118.", and fullListHref to follow it with the full list's link.
API Reference
A nav named “Pagination”. numbered and compact: Previous at the
start, the page numbers (or the “Page 5 of 12″ status) centered, Next at
the end; Previous and Next are type-label caps with Material Symbols
chevron_left / chevron_right at the inline tier; page numerals are
mono at type-label size with tabular figures, so a changing count never
shifts the row [D165, D174, D175]. The current page is unlinked, bold and
barred. step and dots: icon-only ‹ › circles around a mono “3 of 12″
(plus up to 8 dots). more: “Showing 20 of 54″ over a “Show More”
Button. At the ends a control is omitted and its space kept, never
disabled. The pager is hidden in print; with items the linked forms
print one state line instead.
| Prop | Type | Description |
|---|---|---|
count | | The page (or item) count. |
fullListHref | | The full list’s URL, printed after the state line per §7.6 (“Full list (example.org/birds)"). |
getHref | | Every page’s URL: each page is a link, never a click handler alone (P8). |
items | | The items on this page. Given, the pager prints “Items 41–50 of 118.” in its place. |
kind | |
|
moreLabel | | |
nextLabel | | The Next label, authored in sentence case; the CSS sets the caps [D165]. On the step pager it is the › button’s accessible name. Default “Next”. |
onPageChange | | |
page | | The current page (or item), 1-based. |
previousLabel | | The Previous label, authored in sentence case; the CSS sets the caps [D165]. On the step pager it is the ‹ button’s accessible name. Default “Previous”. |
primary | | Primary Radix scale: numerals, labels, the current bar and the focus ring. Never defaulted; omitted, it inherits the scope [D133]. |
renderLink | | Builds each anchor, e.g. |
secondary | | Secondary Radix scale: the Previous and Next arrows on light grounds. Never defaulted. |
pagination
type pagination = paginationPaginationItems
The items on the current page, for the printed state line (§7.12.1).
type PaginationItems = {
/** The first item's position on this page, e.g. 41. */
first: number;
/** The last item's position on this page, e.g. 50. */
last: number;
/** The item count across every page, e.g. 118. */
total: number;
}PaginationKind
type PaginationKind = 'numbered' | 'compact' | 'step' | 'dots' | 'more'<
'numbered' | 'compact' | 'step' | 'dots' | 'more' | null | undefined
>PaginationProps
Props for Pagination: nav props, the page, the count, the form and how it moves, and the color axes.
type PaginationProps = (
| {
kind?: 'numbered' | 'compact';
getHref: (page: number) => string;
onPageChange?: undefined;
moreLabel?: undefined;
}
| {
kind: 'step' | 'dots';
getHref?: (page: number) => string;
onPageChange?: (page: number) => void;
moreLabel?: undefined;
}
| {
kind: 'more';
getHref: (page: number) => string;
moreLabel?: string;
onPageChange?: undefined;
}
) & {
/** The current page (or item), 1-based. */
page: number;
/** The page (or item) count. */
count: number;
/**
* Builds each anchor, e.g. `(href) => <NextLink href={href} />`. Default: a
* plain `<a href>`.
*/
renderLink?: (href: string) => ReactElement;
/**
* Primary Radix scale: numerals, labels, the current bar and the focus
* ring. Never defaulted; omitted, it inherits the scope [D133].
*/
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
/** Secondary Radix scale: the Previous and Next arrows on light grounds. Never defaulted. */
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
/**
* The Previous label, authored in sentence case; the CSS sets the caps
* [D165]. On the step pager it is the ‹ button's accessible name. Default
* "Previous".
*/
previousLabel?: string;
/**
* The Next label, authored in sentence case; the CSS sets the caps
* [D165]. On the step pager it is the › button's accessible name. Default
* "Next".
*/
nextLabel?: string;
/** The items on this page. Given, the pager prints "Items 41–50 of 118." in its place. */
items?: PaginationItems;
/** The full list's URL, printed after the state line per §7.6 ("Full list (example.org/birds)"). */
fullListHref?: string;
}PaginationVariants
type PaginationVariants = {
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
kind?: 'numbered' | 'compact' | 'step' | 'dots' | 'more' | null;
}Specification: DESIGN-SYSTEM.md §9.9 (pagination) and §5.10.2 (reflow), with D165, D174 and D175.