SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/pagination

7 CSS Pagination

3 Pure CSS4 with JSPublished 2026-10-05Free · no sign-up

Seven ways to move through results: numbered pages with ellipses, a simple previous and next with Page 4 of 12, a Load more button, a jump-to-page field, pill-style pages, a data table footer with rows per page and infinite scroll that stops at a known end.

01. Numbered Pagination with Ellipsis

PaginationNumbers

Previous and next links around page numbers, with an ellipsis for skipped ranges. It is a nav with an aria-label, the current page is marked with aria-current="page", and the arrows have full names such as Previous page.

View code
<nav class="pg1" aria-label="Pagination"><a href="#p3" aria-label="Previous page">&larr;</a><a href="#p1">1</a><span aria-hidden="true">…</span><a href="#p3">3</a><a href="#p4" aria-current="page">4</a><a href="#p5">5</a><span aria-hidden="true">…</span><a href="#p12">12</a><a href="#p5" aria-label="Next page">&rarr;</a></nav>
.pg1 { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; font: 600 14px Inter, system-ui, sans-serif; }
.pg1 a, .pg1 span { display: grid; place-items: center; min-width: 36px; height: 36px; padding: 0 4px; box-sizing: border-box; border-radius: 8px; color: #344054; text-decoration: none; }
.pg1 a:hover { background: #f2f4f7; }
.pg1 a[aria-current] { background: #101828; color: #fff; }
.pg1 span { color: #475467; min-width: 20px; }
.pg1 a:focus-visible { outline: 3px solid #84adff; outline-offset: 1px; }

02. Previous and Next with Page Count

PaginationSimple

The minimal version for mobile and blogs: Previous, Page 4 of 12 and Next. On the first or last page the unavailable link becomes plain text with aria-disabled rather than a link that goes nowhere.

View code
<nav class="pg2" aria-label="Pagination"><a href="#p3" rel="prev">&larr; Previous</a><p>Page <b>4</b> of <b>12</b></p><a href="#p5" rel="next">Next &rarr;</a></nav>
.pg2 { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 340px; max-width: 100%; font: 14px Inter, system-ui, sans-serif; }
.pg2 a { padding: 8px 12px; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-weight: 600; text-decoration: none; white-space: nowrap; }
.pg2 a:hover { background: #1e293b; }
.pg2 p { margin: 0; color: #94a3b8; } .pg2 b { color: #fff; }
.pg2 a:focus-visible { outline: 2px solid #93c5fd; outline-offset: 2px; }

03. Load More Button

PaginationLoad more

Instead of pages, a Load more button appends the next batch and says how many are showing out of the total. Focus moves to the first new item so keyboard users continue from where the new content starts, and the button disappears when everything is loaded.

View code
<div class="pg3"><ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul><p aria-live="polite">Showing 3 of 9</p><button>Load more</button></div>
.pg3 { width: 280px; max-width: 100%; font: 14px Inter, system-ui, sans-serif; }
.pg3 ul { display: grid; gap: 6px; margin: 0 0 10px; padding: 0; list-style: none; }
.pg3 li { padding: 10px 12px; border: 1px solid #e4e7ec; border-radius: 8px; background: #fff; color: #101828; }
.pg3 li:focus { outline: 3px solid #84adff; outline-offset: 1px; }
.pg3 p { margin: 0 0 8px; color: #475467; font-size: 13px; text-align: center; }
.pg3 button { width: 100%; padding: 10px; border: 1px solid #d0d5dd; border-radius: 8px; background: #fff; color: #101828; font: 700 14px Inter, system-ui, sans-serif; cursor: pointer; }
.pg3 button:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
var ul = document.querySelector('.pg3 ul'), p = document.querySelector('.pg3 p'), b = document.querySelector('.pg3 button'), n = 3, total = 9;
b.onclick = function () { var first; for (var i = 0; i < 3 && n < total; i++) { n++; var li = document.createElement('li'); li.textContent = 'Item ' + n; li.tabIndex = -1; ul.appendChild(li); if (!first) first = li; } p.textContent = 'Showing ' + n + ' of ' + total; if (first) first.focus(); if (n >= total) b.remove(); };

04. Jump to Page Field

PaginationInput

For long result sets, a small form lets people type a page number and go, between the previous and next buttons. The number input has min and max set to the real range and a visible label, and the form submits with Enter.

View code
<form class="pg4"><a href="#p6" aria-label="Previous page">&lsaquo;</a><label>Page <input type="number" min="1" max="48" value="7" inputmode="numeric"> of 48</label><a href="#p8" aria-label="Next page">&rsaquo;</a></form>
.pg4 { display: flex; align-items: center; gap: 10px; color: #cbd5e1; font: 14px Inter, system-ui, sans-serif; }
.pg4 a { display: grid; place-items: center; width: 36px; height: 36px; border: 1px solid #334155; border-radius: 8px; color: #f8fafc; font-size: 20px; text-decoration: none; }
.pg4 label { display: flex; align-items: center; gap: 6px; }
.pg4 input { width: 56px; padding: 7px; border: 1px solid #475569; border-radius: 8px; background: #0f172a; color: #fff; font: 600 14px Inter, system-ui, sans-serif; text-align: center; }
.pg4 a:focus-visible, .pg4 input:focus-visible { outline: 2px solid #93c5fd; outline-offset: 2px; }
document.querySelector('.pg4').addEventListener('submit', function (e) { e.preventDefault(); });

05. Pill Pagination

PaginationStyle

Page links inside one rounded pill container, with the current page as a raised white pill. It is the same accessible markup as plain numbered pagination, so the style can change freely without touching the structure.

View code
<nav class="pg5" aria-label="Pagination"><a href="#p1">1</a><a href="#p2" aria-current="page">2</a><a href="#p3">3</a><a href="#p4">4</a><a href="#p3" aria-label="Next page">Next</a></nav>
.pg5 { display: inline-flex; flex-wrap: wrap; gap: 2px; padding: 4px; border-radius: 999px; background: #f2f4f7; font: 600 14px Inter, system-ui, sans-serif; }
.pg5 a { display: grid; place-items: center; min-width: 36px; height: 34px; padding: 0 10px; box-sizing: border-box; border-radius: 999px; color: #475467; text-decoration: none; }
.pg5 a:hover { color: #101828; }
.pg5 a[aria-current] { background: #fff; color: #101828; box-shadow: 0 1px 3px rgba(16,24,40,.15); }
.pg5 a:focus-visible { outline: 3px solid #84adff; outline-offset: 1px; }

06. Table Footer with Rows per Page

PaginationTable

The footer of a data table: a rows-per-page select, the visible range such as 21-30 of 87, and previous and next buttons. The range text updates as you page, and the buttons are disabled at the ends, which a real disabled attribute communicates.

View code
<div class="pg6"><label>Rows <select><option>10</option><option>25</option><option>50</option></select></label><p aria-live="polite">1-10 of 87</p><div><button aria-label="Previous page" disabled>&lsaquo;</button><button aria-label="Next page">&rsaquo;</button></div></div>
.pg6 { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; width: 360px; max-width: 100%; padding: 10px 12px; box-sizing: border-box; border-top: 1px solid #263048; color: #94a3b8; font: 13px Inter, system-ui, sans-serif; }
.pg6 label { display: flex; align-items: center; gap: 6px; }
.pg6 select { padding: 4px 6px; border: 1px solid #334155; border-radius: 6px; background: #0f172a; color: #e2e8f0; font: inherit; }
.pg6 p { margin: 0; color: #e2e8f0; }
.pg6 div { display: flex; gap: 4px; }
.pg6 button { width: 32px; height: 32px; border: 1px solid #334155; border-radius: 6px; background: #151b2e; color: #f8fafc; font-size: 18px; cursor: pointer; }
.pg6 button:disabled { opacity: .4; cursor: not-allowed; }
.pg6 :focus-visible { outline: 2px solid #93c5fd; outline-offset: 2px; }
var w = document.querySelector('.pg6'), sel = w.querySelector('select'), p = w.querySelector('p'), bs = w.querySelectorAll('button'), total = 87, start = 0;
function draw() { var n = +sel.value, end = Math.min(start + n, total); p.textContent = (start + 1) + '-' + end + ' of ' + total; bs[0].disabled = start === 0; bs[1].disabled = end >= total; }
bs[0].onclick = function () { start = Math.max(0, start - +sel.value); draw(); };
bs[1].onclick = function () { start = start + +sel.value; draw(); };
sel.onchange = function () { start = 0; draw(); };

07. Infinite Scroll with a Sentinel

PaginationInfinite scroll

More items load automatically when a sentinel at the end of the list scrolls into view. A status line says what is happening, the list stops at a known end with a clear message, and a footer stays reachable because loading is capped.

View code
<div class="pg7" tabindex="0" role="region" aria-label="Feed, scroll"><ul></ul><p class="st" role="status">Loading…</p><span class="sent" aria-hidden="true"></span></div>
.pg7 { width: 260px; max-width: 100%; height: 280px; overflow-y: auto; box-sizing: border-box; padding: 10px; border: 1px solid #e4e7ec; border-radius: 14px; background: #fff; font: 14px Inter, system-ui, sans-serif; }
.pg7:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.pg7 ul { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
.pg7 li { padding: 14px 12px; border-radius: 8px; background: #f9fafb; color: #101828; }
.pg7 .st { margin: 10px 0; color: #475467; font-size: 13px; text-align: center; }
.pg7 .sent { display: block; height: 1px; }
var box = document.querySelector('.pg7'), ul = box.querySelector('ul'), st = box.querySelector('.st'), n = 0, max = 30, busy = false;
function more() { if (busy || n >= max) return; busy = true; st.textContent = 'Loading…'; setTimeout(function () { for (var i = 0; i < 6 && n < max; i++) { n++; var li = document.createElement('li'); li.textContent = 'Post ' + n; ul.appendChild(li); } busy = false; st.textContent = n >= max ? 'You have reached the end. 30 posts.' : 'Showing ' + n + ' posts'; }, 400); }
new IntersectionObserver(function (es) { if (es[0].isIntersecting) more(); }, { root: box, rootMargin: '80px' }).observe(box.querySelector('.sent'));

About these CSS Pagination

Pagination is navigation

Wrap page links in <nav aria-label="Pagination">, mark the current page with aria-current="page" and give arrow links real names such as Previous page.

Pages are links when they have URLs

If each page has its own URL, use links, so people can open them in new tabs and search engines can follow them. Use buttons only when the page changes without a new URL.

Load more moves focus

After loading more items, move focus to the first new one and announce the new count. Otherwise keyboard users are left on a button far from the new content.

Infinite scroll needs an end

Endless feeds hide the footer and make it hard to return to an item. Cap automatic loading, say when the end is reached, or offer a Load more button instead.

Frequently asked questions

Which pattern is best for SEO?

Numbered pages with real URLs are the most crawlable.

How many page numbers should I show?

First, last, the current page and one or two either side, with ellipses for the gaps.

Is infinite scroll accessible?

It can be, with a status message and a known end, but Load more is often simpler.