Live: try it with the mouse and the keyboard (Tab, arrows, Escape).

Pagination UI component for websites and web apps, shown in Fieldnote, a fictional media product. Shows numbered pagination with previous and next links, the current page, collapsed gaps and a result range, plus every link state (default, hover, focus-visible, current, disabled). Minimal style: cool neutrals, Inter, 8px corners and soft shadows. Comes with copyable HTML, CSS and a few lines of plain JavaScript (keyboard support and ARIA per the WAI-ARIA Authoring Practices), a live demo, and a DESIGN.md with light and dark colour tokens.
#f7f6fa · surface #edebf4 · text #191720 · muted #64616e · primary #207de4 · accent #765dae#0c0a10 · surface #17151e · text #edecef · muted #b2afbb · primary #8ebfff · accent #b59df3Paste the HTML where the component goes, the CSS into your stylesheet and the script before </body>. The tokens block holds the light and dark colours; swap in your own.
<nav class="mgd-pg" aria-label="Pagination" data-total="1284" data-per="20" data-page="3">
<p class="mgd-pg__range" aria-live="polite">Showing 41–60 of 1,284 photos</p>
<ul class="mgd-pg__list">
<li><a class="mgd-pg__link" href="?page=2" data-page="2"><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span class="mgd-pg__word">Previous</span></a></li>
<li class="mgd-pg__far"><a class="mgd-pg__link" href="?page=1" data-page="1" aria-label="Page 1">1</a></li>
<li class="mgd-pg__far"><a class="mgd-pg__link" href="?page=2" data-page="2" aria-label="Page 2">2</a></li>
<li><a class="mgd-pg__link" href="?page=3" data-page="3" aria-label="Page 3" aria-current="page">3</a></li>
<li class="mgd-pg__far"><a class="mgd-pg__link" href="?page=4" data-page="4" aria-label="Page 4">4</a></li>
<li class="mgd-pg__gap" aria-hidden="true">…</li>
<li class="mgd-pg__far"><a class="mgd-pg__link" href="?page=65" data-page="65" aria-label="Page 65">65</a></li>
<li><a class="mgd-pg__link" href="?page=4" data-page="4"><span class="mgd-pg__word">Next</span><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></a></li>
</ul>
</nav>/* Tokens (light + dark) */
:root{--bg:#f7f6fa;--surface:#edebf4;--border:#cccad6;--text:#191720;--muted:#64616e;--primary:#207de4;--on-primary:#111111;--accent:#765dae;--font-h:"Inter",Georgia,serif;--font-b:"Inter",system-ui,sans-serif;--hw:600;
--c-bw:1px;--c-line:var(--border);--c-r:8px;--c-pill:8px;--c-sh:0 1px 2px rgb(0 0 0 / .06);--c-pop:0 12px 32px rgb(0 0 0 / .14);--c-tt:none;--c-ls:0;--c-fw:500;--c-fs:14px;--c-ring:2px solid var(--primary);--c-ind:2px;color-scheme:light}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#0c0a10;--surface:#17151e;--border:#2c2a33;--text:#edecef;--muted:#b2afbb;--primary:#8ebfff;--on-primary:#111111;--accent:#b59df3;color-scheme:dark}
}
:root[data-theme="dark"]{--bg:#0c0a10;--surface:#17151e;--border:#2c2a33;--text:#edecef;--muted:#b2afbb;--primary:#8ebfff;--on-primary:#111111;--accent:#b59df3;color-scheme:dark}
/* Component */
.mgd-sr{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.mgd-pg{display:grid;gap:14px;justify-items:start}
.mgd-pg__range{margin:0;color:var(--muted);font-size:14px;font-variant-numeric:tabular-nums}
.mgd-pg__list{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.mgd-pg__link{min-width:42px;height:42px;padding:0 12px;display:inline-flex;align-items:center;justify-content:center;gap:6px;border:var(--c-bw) solid var(--c-line);border-radius:var(--c-pill);background:var(--surface);color:var(--text);font:var(--c-fw) var(--c-fs)/1 var(--font-b);text-transform:var(--c-tt);letter-spacing:var(--c-ls);font-variant-numeric:tabular-nums;text-decoration:none;box-shadow:var(--c-sh);transition:background-color .12s,border-color .12s}
a.mgd-pg__link:hover,.mgd-pg__link.is-hover{background:color-mix(in oklab,var(--primary) 10%,var(--surface));border-color:var(--primary)}
.mgd-pg__link:focus-visible,.mgd-pg__link.is-focus{outline:var(--c-ring);outline-offset:2px;position:relative;z-index:1}
.mgd-pg__link[aria-current="page"],.mgd-pg__link.is-on{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}
.mgd-pg__link[aria-disabled="true"],.mgd-pg__link.is-off{opacity:.45;cursor:not-allowed;box-shadow:none}
.mgd-pg__gap{min-width:28px;text-align:center;color:var(--muted)}
@media (max-width:520px){.mgd-pg__far,.mgd-pg__gap{display:none}
.mgd-pg__word{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}}
@media (prefers-reduced-motion:reduce){.mgd-pg,.mgd-pg *{transition:none!important;animation-duration:2.4s!important}}document.querySelectorAll(".mgd-pg").forEach(function (nav) {
var total = +nav.dataset.total, per = +nav.dataset.per, last = Math.ceil(total / per), list = nav.querySelector(".mgd-pg__list"), range = nav.querySelector(".mgd-pg__range");
var prevIcon = list.querySelector("li:first-child svg").outerHTML, nextIcon = list.querySelector("li:last-child svg").outerHTML;
function link(n, label, html, cls) {
var off = n < 1 || n > last;
return "<li" + (cls ? ' class="' + cls + '"' : "") + "><a class=\"mgd-pg__link\"" + (off ? ' role="link" aria-disabled="true"' : ' href="?page=' + n + '" data-page="' + n + '"') + (label ? ' aria-label="' + label + '"' : "") + (n === +nav.dataset.page && label ? ' aria-current="page"' : "") + ">" + html + "</a></li>";
}
function render(p, focus) {
nav.dataset.page = p;
var pages = [1], h = "";
for (var i = Math.max(2, p - 1); i <= Math.min(last - 1, p + 1); i++) pages.push(i);
if (last > 1) pages.push(last);
h += link(p - 1, "", prevIcon + '<span class="mgd-pg__word">Previous</span>');
pages.forEach(function (n, k) {
if (k && n - pages[k - 1] > 1) h += '<li class="mgd-pg__gap" aria-hidden="true">…</li>';
h += link(n, "Page " + n, String(n), n === p ? "" : "mgd-pg__far");
});
h += link(p + 1, "", '<span class="mgd-pg__word">Next</span>' + nextIcon);
list.innerHTML = h;
range.textContent = "Showing " + ((p - 1) * per + 1) + "–" + Math.min(p * per, total) + " of " + total.toLocaleString("en") + " photos";
if (focus) { var cur = list.querySelector("[aria-current]"); if (cur) cur.focus(); }
}
list.addEventListener("click", function (e) {
var a = e.target.closest("a[data-page]"); if (!a) return;
e.preventDefault(); render(+a.dataset.page, true);
});
render(+nav.dataset.page, false);
});## Component: Pagination
Numbered pages with previous and next links, a current page and a collapsed gap for long result sets.
- **Pattern:** Semantic HTML (no ARIA widget role needed)
- **Roles and attributes:** <nav aria-label="Pagination"> around a list of links; the current page has aria-current="page"; previous on page 1 is aria-disabled and out of the tab order; the gap is hidden text, not a link. A polite live region announces the new range.
- **States drawn:** default, hover, focus-visible, selected, disabled
- **Focus:** a visible `:focus-visible` outline (2px solid primary), never removed
- **Style knobs (minimal):** border 1px, corners 8px, shadow `0 1px 2px rgb(0 0 0 / .06)`, labels sentence case
- **Motion:** transitions are short (≤160ms) and switched off under `prefers-reduced-motion: reduce`
- **Code:** HTML + CSS + a few lines of plain JavaScript (no libraries); colours come only from the tokens above, so light and dark both work
### Keyboard
- **Tab / Shift+Tab:** move between page links
- **Enter:** go to the focused page
### Licence
Original Made Good Designs code: free to use and adapt in any project, including commercial ones, with no credit needed. Fonts are Google Fonts under their own licences.Add the Made Good Designs library to your assistant once. Then ask for UI references, brand palettes, font pairings and design tokens in any chat.
https://madegooddesigns.com/inspiration/mcp?ref=ui-pagination-uiSettings → Connectors → Add custom connector → paste the URL.
Settings → Apps & Connectors → Advanced → Developer mode → Create → paste the URL.
One click, then confirm in Cursor.
One click, then confirm in VS Code (Copilot agent mode).
claude mcp add --transport http made-good-designs https://madegooddesigns.com/inspiration/mcp?ref=ui-pagination-ui
gemini mcp add --transport http made-good-designs https://madegooddesigns.com/inspiration/mcp?ref=ui-pagination-ui
Add this to the app's MCP config (Windsurf: mcp_config.json).
Fonts: Inter (Google Fonts)
Prompt copied: paste it into the chat that just opened.
Writing about this component?
search_ui_screens (e.g. “billing settings, editorial style”), get_ui_flow for a whole journey, and get_ui_screen_code to pull the HTML and DESIGN.md straight into your project.