Pagination New Alpha Light DOM & Tailwind
Composition-first navigation primitives for paging through a collection — your app owns the state, Nord owns the semantics, layout and styling.
Usage
Pagination is the navigation root for moving through a paginated collection. It is composition-first: it owns no page state, renders no controls, and emits no events. Your app owns the page state, URLs, cursors and any data fetching; Nord supplies the semantic/layout primitives, the styling, and the paginate() math utility for building the page window.
Import the parts you need — each import registers its custom element:
import "@nordhealth/components/lib/Pagination"
import "@nordhealth/components/lib/PaginationContent"
import "@nordhealth/components/lib/PaginationItem"
import "@nordhealth/components/lib/PaginationLink"
import "@nordhealth/components/lib/PaginationPrevious"
import "@nordhealth/components/lib/PaginationNext"
import "@nordhealth/components/lib/PaginationEllipsis"
Compose the primitives inside the root, bringing your own interactive element — an <a> (e.g. a framework <Link> / <NuxtLink> for crawlable, SSR-friendly links) or a nord-button. You own each link's href, navigation and disabled state. The root sets role="navigation" and a default accessible label for you, so there's no separate <nav> to add.
Pagination is navigation-only. A typical data-table footer pairs the numbered pages with a results summary and a page-size selector — but those are your app's own plain elements (a text node and a nord-select), not pagination sub-components:
The primitives are:
nord-pagination— the navigation root (anavigationlandmark).nord-pagination-content— the list row that lays its items out horizontally.nord-pagination-item— a single slot in the row.nord-pagination-link— a page link; setcurrenton the active page and it mirrorsaria-current="page"onto your link.nord-pagination-previous/nord-pagination-next— the previous/next controls.nord-pagination-ellipsis— the collapsed-pages marker (…) with a hidden label.
Pagination renders in the light DOM, so you can style it directly with your own CSS or Tailwind utilities.
Examples
Simple
Plain numbered pages with no previous/next controls. Mark the active page with current.
Numbered pages
The classic numbered pager: previous, the page-window numbers around the current page, an ellipsis for the collapsed gap, and next. Use the paginate() utility to compute which numbers (and ellipses) to render around the current page.
Icons only
A "Rows per page" selector on one end and icon-only previous/next on the other, justified to the row's two ends. For an icon-only nord-button, put a nord-visually-hidden label inside the button — never an aria-label on the host.
Cursor pagination
For collections with unknown totals (e.g. relay-style cursors), compose previous/next only. Your app enables or disables each control from its API response's page info.
Right-to-left
The primitives use logical properties, so in a right-to-left document they mirror automatically — set dir="rtl" on a wrapping element. Provide localized previous/next labels and page numbers yourself.
Building the page window
For numbered pages with known totals, the paginate() utility returns the sequence of page numbers (and ellipsis markers) to render, keeping the window centred on the current page and collapsing long gaps into a single ellipsis:
import { paginate } from "@nordhealth/components"
// paginate(currentPage, pageCount, pagesShown = 7)
paginate(6, 50) // → [1, "…", 4, 5, 6, 7, 8, "…", 50]
Map the tokens to nord-pagination-link (for numbers) and nord-pagination-ellipsis (for the "…" marker) yourself, setting current on the token that equals the current page.
Framework integration
Pagination is composition-first, so integration is just rendering your own links from your own state. Bind clicks/navigation with whatever your framework provides — there is no custom event to listen for.
Next.js
Use next/link for the page links so navigation is client-side and crawlable. Compute the window with paginate() and read the current page from the URL.
import Link from "next/link"
import { paginate } from "@nordhealth/components"
export function Results({ page, pageCount }: { page: number; pageCount: number }) {
return (
<nord-pagination>
<nord-pagination-content>
{paginate(page, pageCount).map((token, i) =>
token === "…" ? (
<nord-pagination-item key={`gap-${i}`}>
<nord-pagination-ellipsis />
</nord-pagination-item>
) : (
<nord-pagination-item key={token}>
{/* `current` mirrors aria-current onto the <a> */}
<nord-pagination-link current={token === page}>
<Link href={`?page=${token}`}>{token}</Link>
</nord-pagination-link>
</nord-pagination-item>
)
)}
</nord-pagination-content>
</nord-pagination>
)
}
Nuxt.js
Use <NuxtLink> for the page links and read the current page from the route.
<script setup lang="ts">
import { paginate } from "@nordhealth/components"
const props = defineProps<{ pageCount: number }>()
const route = useRoute()
const page = computed(() => Number(route.query.page ?? 1))
const tokens = computed(() => paginate(page.value, props.pageCount))
</script>
<template>
<nord-pagination>
<nord-pagination-content>
<nord-pagination-item v-for="(token, i) in tokens" :key="i">
<nord-pagination-ellipsis v-if="token === '…'" />
<nord-pagination-link v-else :current="token === page">
<NuxtLink :to="{ query: { ...route.query, page: token } }">{{ token }}</NuxtLink>
</nord-pagination-link>
</nord-pagination-item>
</nord-pagination-content>
</nord-pagination>
</template>
TanStack Table
TanStack owns the table's 0-based pageIndex; use paginate() only for the visible page window and map page ↔ pageIndex at the boundary.
import { paginate } from "@nordhealth/components"
const pageCount = table.getPageCount()
const page = table.getState().pagination.pageIndex + 1 // 1-based for paginate()
const tokens = paginate(page, pageCount)
// Render each numeric token as a nord-pagination-link whose click calls:
// table.setPageIndex(token - 1)
// and the ellipsis token as a nord-pagination-ellipsis.
Accessibility
- The root is a
navigationlandmark with a default accessible label, so screen-reader users can jump to it. Override it with your ownaria-labeloraria-labelledbywhen a page has more than one pager. - Mark the active page by setting
currenton itsnord-pagination-link; it mirrorsaria-current="page"onto your link. - For icon-only previous/next controls, put a
nord-visually-hiddenlabel inside thenord-button(never anaria-labelon the host). Disable a control when there is nowhere to go. - The ellipsis glyph is hidden from assistive tech and carries a visually-hidden label; localise it with the
labelproperty.
API reference
Pagination
Pagination is the navigation root for a paginated collection. It is
composition-first: it owns no page state, renders no controls, and emits no
events. Your app owns the page state, URL/query syncing, cursors and any data
fetching; Nord supplies the semantic/layout primitives, the styling and the
paginate() math utility for building the page window.
As a root it is a navigation landmark — it sets role="navigation" and a
default accessible label so screen-reader users can jump to it. Compose the
lower-level primitives inside it:
- Pagination Content — the list row.
- Pagination Item — a slot in the row.
- Pagination Link — a page link (set current).
- Pagination Previous — the previous control.
- Pagination Next — the next control.
- Pagination Ellipsis — the collapsed-pages marker.
Bring your own interactive element — an <a> (e.g. a framework <Link> /
<NuxtLink>) or a nord-button — and own its href, navigation and
disabled state. See the framework adapters in the docs.
<nord-pagination></nord-pagination>Props
| Property | Attribute | Description | Type | Default |
|---|---|---|---|---|
loading | loading | Dims the pagination and disables interaction while results are loading. | boolean | false |
Slots
| Slot name | Description |
|---|---|
Default slot | Default slot for the composed pagination primitives. |
CSS Properties
CSS Custom Properties provide more fine grain control over component presentation. We advise utilizing existing properties on the component before using these.
| Property | Description | Default |
|---|---|---|
--n-pagination-gap | Controls the spacing between pagination controls. | var(--n-space-xs) |
Parts
This component is made up of the following parts.
Pagination Content
The list container for composed Pagination controls.
Lays its Pagination Item children out in a row.
Place it inside Pagination, which is already the
navigation landmark (it sets role="navigation" and an accessible label).
Do not add your own <nav> wrapper — a second landmark around the same
controls would be a duplicate that screen-reader users have to wade through.
<nord-pagination-content></nord-pagination-content>Slots
| Slot name | Description |
|---|---|
Default slot | Default slot for Pagination Item children. |
Pagination Ellipsis
The collapsed-pages marker (…) in a composed Pagination.
The glyph is decorative; a visually-hidden label gives it meaning for
assistive tech. Set label to localise it.
<nord-pagination-ellipsis></nord-pagination-ellipsis>Props
| Property | Attribute | Description | Type | Default |
|---|---|---|---|---|
label | label | The visually-hidden label announced for the collapsed pages. | string | 'More pages' |
CSS Properties
CSS Custom Properties provide more fine grain control over component presentation. We advise utilizing existing properties on the component before using these.
| Property | Description | Default |
|---|---|---|
--n-pagination-gap | Inherited spacing token used by the pagination row. | var(--n-space-xs) |
Pagination Item
A single slot in a composed Pagination Content row. Wrap each control — a Pagination Link, Pagination Previous, Pagination Next or Pagination Ellipsis — in its own item.
<nord-pagination-item></nord-pagination-item>Slots
| Slot name | Description |
|---|---|
Default slot | Default slot for a single pagination control. |
Pagination Link
A page link in a composed Pagination. A layout
wrapper around your own interactive element — provide an <a> (e.g. a
framework <Link> for crawlable, SSR-friendly links) or a nord-button. Set
current on the link for the active page; it mirrors aria-current="page"
onto the interactive child so assistive tech announces it.
<nord-pagination-link></nord-pagination-link>Props
| Property | Attribute | Description | Type | Default |
|---|---|---|---|---|
current | current | Marks this as the current page, mirroring aria-current="page" onto the child. | boolean | false |
Slots
| Slot name | Description |
|---|---|
Default slot | Default slot for the page link or button. |
Pagination Next
The "next page" control in a composed Pagination. A
layout wrapper around your own interactive element — provide an <a> (e.g. a
framework <Link>) or a nord-button with its own icon, label and
navigation, and disable it when there is no next page.
<nord-pagination-next></nord-pagination-next>Slots
| Slot name | Description |
|---|---|
Default slot | Default slot for the next-page link or button. |
Pagination Previous
The "previous page" control in a composed Pagination.
A layout wrapper around your own interactive element — provide an <a>
(e.g. a framework <Link>) or a nord-button with its own icon, label and
navigation, and disable it when there is no previous page.
<nord-pagination-previous></nord-pagination-previous>Slots
| Slot name | Description |
|---|---|
Default slot | Default slot for the previous-page link or button. |
Design guidelinesFor designers
Usage
Pagination is the navigation root for moving through a paginated collection. It is composition-first: it owns no page state, renders no controls, and emits no events. Your app owns the page state, URLs, cursors and any data fetching; Nord supplies the semantic/layout primitives, the styling, and the paginate() math utility for building the page window.
import '@nordhealth/components/lib/Pagination'
Compose the primitives inside it, bringing your own interactive element — an <a> (e.g. a framework <Link> / <NuxtLink> for crawlable, SSR-friendly links) or a nord-button. You own each link's href, navigation and disabled state. Mark the active page with current on the nord-pagination-link and it mirrors aria-current="page" onto your link.
<nord-pagination>
<nord-pagination-content>
<nord-pagination-item>
<nord-pagination-previous>
<nord-button square href="?page=1">
<nord-icon name="arrow-left-small"></nord-icon>
<nord-visually-hidden>Previous</nord-visually-hidden>
</nord-button>
</nord-pagination-previous>
</nord-pagination-item>
<nord-pagination-item>
<nord-pagination-link current>
<nord-button variant="primary" href="?page=1">1</nord-button>
</nord-pagination-link>
</nord-pagination-item>
<nord-pagination-item>
<nord-pagination-link>
<nord-button variant="plain" href="?page=2">2</nord-button>
</nord-pagination-link>
</nord-pagination-item>
<nord-pagination-item>
<nord-pagination-ellipsis></nord-pagination-ellipsis>
</nord-pagination-item>
<nord-pagination-item>
<nord-pagination-next>
<nord-button square href="?page=2">
<nord-icon name="arrow-right-small"></nord-icon>
<nord-visually-hidden>Next</nord-visually-hidden>
</nord-button>
</nord-pagination-next>
</nord-pagination-item>
</nord-pagination-content>
</nord-pagination>
The root sets role="navigation" and a default accessible label, so you don't need to wrap it in a <nav>. It renders in the light DOM, so you can style it directly with your own CSS or Tailwind utilities.
Building the page window
For numbered pages with known totals, the paginate() utility returns the sequence of page numbers (and ellipsis markers) to render, keeping the window centred on the current page:
import { paginate } from '@nordhealth/components'
paginate(currentPage, pageCount) // → [1, "…", 4, 5, 6, "…", 50]
Map the tokens to nord-pagination-link / nord-pagination-ellipsis yourself — see the docs page for Next.js, Nuxt and TanStack adapters.
Do
- Treat your app as the source of truth: own page state, URLs and cursors, and supply the links.
- Use
paginate()to compute the numbered page window for known totals. - Set
currenton the activenord-pagination-linkso it announces as the current page. - For icon-only previous/next, put a
nord-visually-hiddenlabel inside thenord-button.
Don't
- Don't expect it to render controls or change page on its own — you supply the markup.
- Don't put an
aria-labelon anord-buttonhost; use anord-visually-hiddenchild instead. - Don't add a second
<nav>around the root — it is already a navigation landmark.