Page links with prev/next and ellipses for long page ranges.

Last updated

Show code
<Pagination currentPage={4} totalPages={10} />

Add

npx @ava-ui/apogee add pagination

No Apogee component dependencies. Ships @lucide/astro for the prev/next chevrons. Zero JS — plain links.

Usage

---
import Pagination from '@/components/apogee/Pagination/Pagination.astro'
---
<Pagination currentPage={1} totalPages={5} />

API

@composable — exposed to Content Editors in AVA. Any other attribute passes through to the root <nav>.

Prop Type Default Description
currentPage number — Current page, 1-indexed.
totalPages number — Total number of pages.
getHref (page: number) => string (p) => `?page=${p}` Builds each page link’s href.
previousLabel / nextLabel string 'Previous page' / 'Next page' Accessible names of the arrows, or their visible text with labels (localise them).
labels boolean false Shows the labels beside the arrows from md; below it they stay as screen-reader text.
rounded boolean false Round page cells.
summary string 'Page {current} of {total}' The phone summary; {current} and {total} are replaced (localise it).
class string — Extra classes on the root <nav>, merged last.

Page list shown: always seven cells from seven pages up (first, last, a three-page run around the current page, and an ellipsis or the neighbour on each side), so the row never changes width as the user pages through.

Constant width

With seven or more pages the row always has seven cells, so it never jumps as the user pages through.

Show code
<Pagination currentPage={1} totalPages={20} />
<Pagination currentPage={5} totalPages={20} />
<Pagination currentPage={20} totalPages={20} />

Phones

Below md the page numbers step aside and a “Page 5 of 20” summary sits between the arrows. Resize the window to see it.

Labels and rounded cells

Show code
<Pagination currentPage={3} totalPages={10} labels previousLabel="Previous" nextLabel="Next" />
<Pagination currentPage={3} totalPages={10} rounded />

Localised summary (summary takes {current} and {total}):

Show code
<Pagination currentPage={3} totalPages={10} summary="Pagina {current} din {total}" />

States

First page — the prev control is a disabled, non-interactive <span>:

Show code
<Pagination currentPage={1} totalPages={10} />

Last page — same treatment on next:

Show code
<Pagination currentPage={10} totalPages={10} />

Few pages — no ellipsis needed:

Show code
<Pagination currentPage={2} totalPages={3} />

Current page in the middle of a long range — ellipses on both sides:

Show code
<Pagination currentPage={12} totalPages={24} />

Examples

A table footer with a page-size select, all zero JS: the form submits with GET and the server renders the new page.

<form method="get" class="flex flex-wrap items-center justify-between gap-3 border-t border-neutral px-4 py-3">
  <label class="flex items-center gap-2 text-sm text-neutral-faded">
    Rows per page
    <select name="per_page" onchange="this.form.requestSubmit()">…</select>
  </label>
  <Pagination currentPage={page} totalPages={pages} getHref={(p) => `?page=${p}&per_page=${perPage}`} />
</form>

Route-based links instead of a query param:

<Pagination currentPage={2} totalPages={8} getHref={(p) => `/blog/page/${p}`} />

Accessibility

  • <nav aria-label="Pagination"> landmark.
  • Every page is a real <a> — keyboard and AT navigable with no custom key handling.
  • Current page gets aria-current="page".
  • Disabled edges (currentPage === 1 / === totalPages) render as <span aria-hidden="true" aria-disabled="true">, not links: they hold their place (and their label with labels) but there is nothing to focus or announce.
  • rel="prev" / rel="next" on the adjacent links hint the sequential relationship to browsers and AT.
  • Chevron icons are aria-hidden; the links carry aria-label="Previous page" / "Next page".
  • No JS — server-rendered links, identical behaviour with JS off.

Tokens

Token Used for
--radius-md Cell corner radius
--outline-color-focus Focus ring
--background-neutral-faded-highlighted Hover background
--background-primary / --on-primary Current-page fill
--foreground-neutral Link text
--foreground-neutral-faded / --foreground-disabled Ellipsis / disabled-edge colour

Do / Don’t

  • Do keep getHref pure — a real, navigable, crawlable URL, not a client-only stub.
  • Do use it for genuine paginated routes/lists, not to fake a “load more” interaction.
  • Don’t intercept the links with a click handler for client-side-only paging — it breaks no-JS and deep-linking.
  • Don’t render it when there’s only one page.

Content

  • No editable copy — page numbers are the content, kept tabular-nums so digit widths stay equal as the current page changes.