Pagination
Pagination breaks long lists into pages, shows where you are, and offers controls to move between pages. The package is composed of a row, previous/next chevrons, a page container, and individual page controls. The authoritative package is @camp/pagination.
Loading...
Overview
Resources
Install
yarn add @camp/paginationUpgrading to Next Gen
🧩 One package, named exports: Replace @activecampaign/camp-components-pagination with @camp/pagination. You can optionally use named exports (PaginationRow, PaginationButton, PaginationPage, PaginationPageContainer) but the same subcomponents exist (Row, Button, Page, and PageContainer) if you prefer shorter names.
🎨 Next Gen styling and tokens: Layout and colors follow the Camp Design System; chevrons use token-based icons and styled-components.
🔄 You control the active page: The container still expects active (0-based index) and textOnly when you need the compact “Page a/b” label instead of the full number strip. Ellipsis treatment applies automatically when there are more than seven pages.
Previous implementation
import * as Pagination from '@activecampaign/camp-components-pagination';
<Pagination.PaginationRow>
<Pagination.PaginationButton prev onClick={() => setActive((i) => Math.max(0, i - 1))} />
<Pagination.PaginationPageContainer active={active} textOnly={false}>
{pages.map((n, i) => (
<Pagination.PaginationPage key={n} active={active === i} onClick={() => setActive(i)}>
{n}
</Pagination.PaginationPage>
))}
</Pagination.PaginationPageContainer>
<Pagination.PaginationButton
prev={false}
onClick={() => setActive((i) => Math.min(pages.length - 1, i + 1))}
/>
</Pagination.PaginationRow>;New implementation (optional named exports)
import {
PaginationButton,
PaginationPage,
PaginationPageContainer,
PaginationRow,
} from '@camp/pagination';Variations
Composed Row (Playground)
A typical wiring: PaginationRow wraps previous/next PaginationButton and a PaginationPageContainer that receives one PaginationPage per page. The parent component owns the active index and updates it from chevrons and number clicks.
import { useState } from 'react';
import {
PaginationButton,
PaginationPage,
PaginationPageContainer,
PaginationRow,
} from '@camp/pagination';
const pages = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
export const Paginated = () => {
const [active, setActive] = useState(0);
return (
<PaginationRow>
<PaginationButton prev onClick={() => setActive((i) => Math.max(0, i - 1))} />
<PaginationPageContainer active={active} textOnly={false}>
{pages.map((page, i) => (
<PaginationPage key={page} active={active === i} onClick={() => setActive(i)}>
{page}
</PaginationPage>
))}
</PaginationPageContainer>
<PaginationButton
prev={false}
onClick={() => setActive((i) => Math.min(pages.length - 1, i + 1))}
/>
</PaginationRow>
);
};Many Pages (Ellipsis)
With more than seven PaginationPage children, the container collapses the list with ... placeholders. Active page, neighbors, and ends stay visible. Ellipsis controls are not interactive and render as disabled buttons (see Accessibility).
const manyPages = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const active = 4;
<PaginationRow>
<PaginationButton prev />
<PaginationPageContainer active={active} textOnly={false}>
{manyPages.map((page, i) => (
<PaginationPage key={page} active={i === active} onClick={() => {}}>
{page}
</PaginationPage>
))}
</PaginationPageContainer>
<PaginationButton prev={false} />
</PaginationRow>;Text Only
Use textOnly on PaginationPageContainer when space is limited (for example, small viewports). The full strip is hidden and a Page a/b label is shown; keep the same children and active index so the label stays in sync when users move with the chevrons.
import { useState } from 'react';
import {
PaginationButton,
PaginationPage,
PaginationPageContainer,
PaginationRow,
} from '@camp/pagination';
const pages = [1, 2, 3, 4, 5];
const TextOnlyExample = () => {
const [active, setActive] = useState(2);
return (
<PaginationRow>
<PaginationButton prev onClick={() => setActive((a) => Math.max(0, a - 1))} />
<PaginationPageContainer active={active} textOnly>
{pages.map((page, i) => (
<PaginationPage key={page} active={i === active} onClick={() => setActive(i)}>
{page}
</PaginationPage>
))}
</PaginationPageContainer>
<PaginationButton
prev={false}
onClick={() => setActive((a) => Math.min(pages.length - 1, a + 1))}
/>
</PaginationRow>
);
};Disabled Previous (Boundary)
Disable the previous chevron on the first page (and the next chevron on the last page) so the UI matches boundaries. The icon buttons use title for screen-reader support (Go to previous page / Go to next page).
<PaginationRow>
<PaginationButton prev disabled />
<PaginationPageContainer active={0} textOnly={false}>
{[1, 2, 3, 4, 5].map((page, i) => (
<PaginationPage key={page} active={i === 0} onClick={() => {}}>
{page}
</PaginationPage>
))}
</PaginationPageContainer>
<PaginationButton prev={false} />
</PaginationRow>Usage
- State: Store the current page as a 0-based index. Pass it to
PaginationPageContainerasactiveand to eachPaginationPageasactive={active === index}. - Chevrons:
PaginationButtonwithprev(defaulttrue) renders the back chevron; setprev={false}for next. Usedisabledon the first and last page as needed. - Text strip:
textOnlyreplaces the number strip with aPage a/blabel. You must still passPaginationPagechildren so the container can compute the total; navigation is usually handled only via chevrons in this mode. - Composition:
PaginationPageContainerwithrole="navigation"andtextOnly={false}wraps the interactive strip; the row is layout-only and does not manage focus.
import { useState } from 'react';
import {
PaginationButton,
PaginationPage,
PaginationPageContainer,
PaginationRow,
} from '@camp/pagination';
const PAGE_NUMBERS = [1, 2, 3, 4, 5, 6, 7];
export const ListPagination = () => {
const [active, setActive] = useState(0);
const last = PAGE_NUMBERS.length - 1;
return (
<PaginationRow>
<PaginationButton
prev
disabled={active === 0}
onClick={() => setActive((i) => Math.max(0, i - 1))}
/>
<PaginationPageContainer active={active} textOnly={false}>
{PAGE_NUMBERS.map((n, i) => (
<PaginationPage key={n} active={active === i} onClick={() => setActive(i)}>
{n}
</PaginationPage>
))}
</PaginationPageContainer>
<PaginationButton
prev={false}
disabled={active === last}
onClick={() => setActive((i) => Math.min(last, i + 1))}
/>
</PaginationRow>
);
};Best Practices
- Do wire chevrons and
PaginationPageonClickto the sameactivestate so the list and the controls never drift. - Do pass two or more
PaginationPagechildren; the container is built for multi-page lists. - Do set
textOnlywhen the full strip does not fit. - Don’t use pagination when everything fits on one page; it adds noise and focus stops.
- Don’t make ellipsis placeholders interactive—they are not clickable by design and render disabled.
Content Guidelines
✅ DO
-
Use pagination for long tables or long card lists with a clear page size.
-
Disable chevrons on the first and last page so boundaries are obvious.
🚫 DON’T
-
Add pagination when a single scroll, load more, or a virtual list is simpler.
-
Expect the
...controls to jump pages; they are not interactive.
Accessibility
Landmarks and State
- Region: When
textOnlyisfalse,PaginationPageContainerrendersrole="navigation"andaria-label="pagination". IntextOnlymode it uses aLabelwith a visiblePage a/bstring and does not use that navigation role on the same structure. - Current page: The active
PaginationPagesetsaria-current="page". Ellipsis rows use disabled buttons, do not receivearia-current, and use the visible...text. - Chevrons:
PaginationButtonuses anIconwith atitleofGo to previous pageorGo to next page.
Keyboard Support
- Tab moves focus through chevrons and number buttons in order.
- Enter and Space on a focused page number run the same action as a click.

