UI Components Medium
Pagination
Page navigation controls with first/prev/next/last, ellipsis for large page counts, page size selector, and a simple mini variant.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 600px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2rem;
}
.section {
margin-bottom: 2rem;
}
.section-label {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #475569;
margin-bottom: 0.75rem;
}
/* ── Pagination ── */
.pagination {
display: flex;
align-items: center;
gap: 0.25rem;
flex-wrap: wrap;
}
.pg-btn {
min-width: 36px;
height: 36px;
padding: 0 0.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.5rem;
background: transparent;
color: #94a3b8;
font-size: 0.85rem;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.pg-btn:hover:not(:disabled):not(.pg-btn--active):not(.pg-btn--ellipsis) {
background: rgba(255, 255, 255, 0.06);
color: #f2f6ff;
}
.pg-btn--active {
background: #38bdf8;
border-color: #38bdf8;
color: #0f172a;
font-weight: 700;
}
.pg-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.pg-btn--ellipsis {
border: none;
background: none;
cursor: default;
letter-spacing: 0.1em;
}
/* Mini */
.pagination--mini .pg-btn {
font-size: 0.8rem;
}
.pg-info {
font-size: 0.82rem;
color: #64748b;
padding: 0 0.5rem;
}
/* With size */
.pg-with-size {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
}
.pg-size-select {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: #64748b;
}
.pg-select {
padding: 0.375rem 0.5rem;
background: #0d1117;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
color: #f2f6ff;
font-size: 0.8rem;
cursor: pointer;
outline: none;
}(function () {
function buildPages(current, total) {
var pages = [];
if (total <= 7) {
for (var i = 1; i <= total; i++) pages.push(i);
return pages;
}
pages.push(1);
if (current > 3) pages.push("...");
for (var i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++)
pages.push(i);
if (current < total - 2) pages.push("...");
pages.push(total);
return pages;
}
function renderFull(el, state) {
var pages = buildPages(state.current, state.total);
el.innerHTML = "";
function btn(label, page, disabled, active, ellipsis) {
var b = document.createElement("button");
b.className =
"pg-btn" + (active ? " pg-btn--active" : "") + (ellipsis ? " pg-btn--ellipsis" : "");
b.textContent = label;
if (disabled || ellipsis) b.disabled = true;
if (!disabled && !ellipsis && !active) {
b.addEventListener("click", function () {
state.current = page;
renderFull(el, state);
});
}
return b;
}
el.appendChild(btn("«", 1, state.current === 1, false, false));
el.appendChild(btn("‹", state.current - 1, state.current === 1, false, false));
pages.forEach(function (p) {
if (p === "...") el.appendChild(btn("…", null, false, false, true));
else el.appendChild(btn(p, p, false, p === state.current, false));
});
el.appendChild(btn("›", state.current + 1, state.current === state.total, false, false));
el.appendChild(btn("»", state.total, state.current === state.total, false, false));
}
function renderMini(el, state) {
el.innerHTML = "";
function btn(label, page, disabled) {
var b = document.createElement("button");
b.className = "pg-btn";
b.textContent = label;
b.disabled = disabled;
if (!disabled)
b.addEventListener("click", function () {
state.current = page;
renderMini(el, state);
});
return b;
}
var info = document.createElement("span");
info.className = "pg-info";
info.textContent = "Page " + state.current + " of " + state.total;
el.appendChild(btn("‹", state.current - 1, state.current === 1));
el.appendChild(info);
el.appendChild(btn("›", state.current + 1, state.current === state.total));
}
var stFull = { current: 8, total: 20 };
var stMini = { current: 4, total: 20 };
var stSize = { current: 2, total: 20 };
renderFull(document.getElementById("pg-full"), stFull);
renderMini(document.getElementById("pg-mini"), stMini);
renderFull(document.getElementById("pg-size"), stSize);
var sizeSel = document.getElementById("pg-size-sel");
if (sizeSel) {
sizeSel.addEventListener("change", function () {
var perPage = parseInt(sizeSel.value);
stSize.total = Math.ceil(100 / perPage);
stSize.current = 1;
renderFull(document.getElementById("pg-size"), stSize);
});
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pagination</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Pagination</h1>
<p class="demo-sub">Page navigation with ellipsis, mini, and page-size variants.</p>
<section class="section">
<p class="section-label">Full pagination</p>
<nav class="pagination" id="pg-full" aria-label="Page navigation"></nav>
</section>
<section class="section">
<p class="section-label">Mini</p>
<nav class="pagination pagination--mini" id="pg-mini" aria-label="Page navigation"></nav>
</section>
<section class="section">
<p class="section-label">With page-size selector</p>
<div class="pg-with-size">
<nav class="pagination" id="pg-size" aria-label="Page navigation"></nav>
<div class="pg-size-select">
<span>Show</span>
<select class="pg-select" id="pg-size-sel">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</select>
<span>per page</span>
</div>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>Pagination
Navigate through large datasets split across multiple pages.
Variants
- Full — page numbers with ellipsis:
« 1 2 … 8 9 10 … 19 20 » - Mini —
← Page 4 of 20 → - With page-size selector — Show 10 / 25 / 50 per page
Ellipsis logic
Always show first/last page. Show up to 3 pages around current. Insert … where pages are skipped. State is fully reactive — clicking any page re-renders the control.