← Back to UI screens

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

ScreenshotPagination component — mobile, editorial style
Theme

Pagination component — mobile, editorial style

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). Editorial style: warm paper tones, Instrument Serif headlines with Inter body text and tabular figures, square corners. 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.

Open live HTML ↗ Download DESIGN.md
Pattern
Pagination
Device
Mobile
Style
Editorial
Product (fictional)
Fieldnote
Category
Media

Spec

Colours (light)
background #fffbf4 · surface #f9eeda · text #2c210e · muted #6f614a · primary #a02b24 · accent #00979e
Colours (dark)
background #1b1204 · surface #2a200e · text #f9f2e7 · muted #bcaf9a · primary #eb7c6f · accent #35cdd5
Fonts
Instrument Serif (headings), Inter (body), JetBrains Mono (code) · Google Fonts, under their own licences
Light and dark
Both included; follows the system setting
ARIA pattern
Semantic HTML, no widget role needed
Keyboard
Tab between links; Enter goes to a page
States
default, hover, focus-visible, selected, disabled
paginationmobilemedia

Code

Paste 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.

HTML
<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>
CSS
/* Tokens (light + dark) */
:root{--bg:#fffbf4;--surface:#f9eeda;--border:#dacdb7;--text:#2c210e;--muted:#6f614a;--primary:#a02b24;--on-primary:#ffffff;--accent:#00979e;--font-h:"Instrument Serif",Georgia,serif;--font-b:"Inter",system-ui,sans-serif;--hw:400;
--c-bw:1px;--c-line:var(--text);--c-r:2px;--c-pill:2px;--c-sh:none;--c-pop:0 10px 30px rgb(0 0 0 / .12);--c-tt:none;--c-ls:0;--c-fw:500;--c-fs:14px;--c-ring:2px solid var(--text);--c-ind:1px;color-scheme:light}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#1b1204;--surface:#2a200e;--border:#413624;--text:#f9f2e7;--muted:#bcaf9a;--primary:#eb7c6f;--on-primary:#111111;--accent:#35cdd5;color-scheme:dark}
}
:root[data-theme="dark"]{--bg:#1b1204;--surface:#2a200e;--border:#413624;--text:#f9f2e7;--muted:#bcaf9a;--primary:#eb7c6f;--on-primary:#111111;--accent:#35cdd5;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}}
JavaScript
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);
});

DESIGN.md

Component section (states, ARIA, keyboard)
## 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 text), never removed
- **Style knobs (editorial):** border 1px, corners 2px, shadow none, 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.
AI

Build this with your AI

Fonts: Instrument Serif + Inter (Google Fonts)

Writing about this component?

More pagination components

Pagination component — web, editorial (Fieldnote)
web · editorial
Pagination component — mobile, minimal (Fieldnote)
mobile · minimal
Pagination component — mobile, brutalist (Fieldnote)
mobile · brutalist
Pagination component — mobile, luxury (Fieldnote)
mobile · luxury
Pagination component — mobile, playful (Fieldnote)
mobile · playful
Pagination component — mobile, corporate (Fieldnote)
mobile · corporate
Pagination component — mobile, retro (Fieldnote)
mobile · retro
Pagination component — mobile, monochrome (Fieldnote)
mobile · monochrome
Building with an AI coding agent? Every screen here is also in the free Made Good Designs MCP server: ask Claude, Cursor or ChatGPT to 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.

Save to board