Pagination
Guidelines
The Pagination component enables users to select a specific page from a range of pages, helping them navigate large sets of content in a clear and manageable way.
Main Component
Code
nav.pagination ul.pagination-items {
display: flex;
flex-direction: row;
justify-content: center;
padding: 0;
list-style: none;
gap: 0.5rem;
margin: 0;
}
.pagination .pagination-items .pagebutton {
width: var(--token-pagination-button-size);
height: var(--token-pagination-button-size);
margin: 0;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background: none;
border: none;
cursor: pointer;
transition: 0.3s;
/* pageicon can be <a> or <button> depending on JS */
.pageicon {
display: flex;
align-items: center;
justify-content: center;
color: var(--token-pagination-icon-default);
/* if it is a button, reset default button styles */
background: transparent;
border: 0;
padding: 0;
font: inherit;
line-height: inherit;
cursor: inherit;
}
&:hover,
&.hover {
background-color: var(--token-pagination-bg-hover);
}
/* focus is usually on inner button, so use focus-within */
&:focus-within,
&.focused {
background-color: var(--token-pagination-bg-focus);
}
&:active,
&.selected {
background-color: var(--token-pagination-bg-selected);
.pageicon {
color: var(--token-pagination-icon-active);
}
}
&:disabled,
&.disabled {
cursor: not-allowed;
.pageicon {
color: var(--token-pagination-icon-disabled);
}
}
}
.pagination-items .pagebutton.-ctrl {
background: none;
border: var(--token-pagination-border-default);
.pageicon .twemoji {
height: var(--token-pagination-icon-size);
width: var(--token-pagination-icon-size);
}
&:hover,
&.hover {
border: 0;
background-color: var(--token-pagination-bg-hover);
}
&:disabled,
&.disabled {
border: var(--token-pagination-border-disabled);
background: none;
cursor: not-allowed;
.pageicon {
color: var(--token-pagination-icon-disabled);
}
}
}
.pagination-items .pagebutton.-more {
cursor: default;
pointer-events: none;
.pageicon .twemoji {
width: var(--token-pagination-icon-size);
height: var(--token-pagination-icon-size);
}
}
<div class="pagination">
<ul class="pagination-items">
<li class="pagebutton -ctrl"><a class="pageicon">:material-chevron-left:</a></li>
<li class="pagebutton selected"><a class="pageicon">1</a></li>
<li class="pagebutton"><a class="pageicon">2</a></li>
<li class="pagebutton -more"><a class="pageicon">:material-dots-horizontal:</a></li>
<li class="pagebutton"><a class="pageicon">5</a></li>
<li class="pagebutton -ctrl"><a class="pageicon">:material-chevron-right:</a></li>
</ul>
</div>
document.addEventListener("DOMContentLoaded", () => {
const pagination = document.querySelector(".pagination");
if (!pagination) return;
const list = pagination.querySelector(".pagination-items");
if (!list) return;
// total pages: read from the largest number already in the HTML (e.g., "10")
const nums = Array.from(
list.querySelectorAll(".pagebutton:not(.-ctrl):not(.-more) .pageicon")
)
.map((el) => parseInt(el.textContent, 10))
.filter(Number.isFinite);
const totalPages = nums.length ? Math.max(...nums) : 10;
// current page: read from .selected if present, else 1
const selected = list.querySelector(".pagebutton.selected .pageicon");
let currentPage = selected ? parseInt(selected.textContent, 10) : 1;
if (!Number.isFinite(currentPage)) currentPage = 1;
const prevIcon =
list.querySelector(".pagebutton.-ctrl:first-child .pageicon")?.innerHTML || "‹";
const nextIcon =
list.querySelector(".pagebutton.-ctrl:last-child .pageicon")?.innerHTML || "›";
function clamp(n, min, max) {
return Math.max(min, Math.min(max, n));
}
function liButton(className, html, data = {}) {
const li = document.createElement("li");
li.className = className;
const btn = document.createElement("button");
btn.type = "button";
btn.className = "pageicon";
btn.innerHTML = html;
Object.entries(data).forEach(([k, v]) => {
li.dataset[k] = String(v);
});
li.appendChild(btn);
return li;
}
function render() {
list.innerHTML = "";
// Prev
const prev = liButton("pagebutton -ctrl", prevIcon, { role: "prev" });
if (currentPage === 1) prev.classList.add("disabled");
list.appendChild(prev);
// Window behavior you requested:
// - Start: 1 2 3 4 … last
// - Sliding: current current+1 current+2 current+3 … last
// - End: … last-4 last-3 last-2 last-1 last
if (totalPages <= 5) {
for (let p = 1; p <= totalPages; p++) {
const item = liButton("pagebutton", String(p), { page: p });
if (p === currentPage) item.classList.add("selected");
list.appendChild(item);
}
} else {
if (currentPage <= 1) currentPage = 1;
// End case: show "... 6 7 8 9 10" style
if (currentPage >= totalPages - 4) {
list.appendChild(liButton("pagebutton -more", "…", { role: "more" }));
for (let p = totalPages - 4; p <= totalPages; p++) {
const item = liButton("pagebutton", String(p), { page: p });
if (p === currentPage) item.classList.add("selected");
list.appendChild(item);
}
} else {
// Normal case: show sliding 4-number window + "... last"
const start = clamp(currentPage, 1, totalPages - 4);
for (let p = start; p <= start + 3; p++) {
const item = liButton("pagebutton", String(p), { page: p });
if (p === currentPage) item.classList.add("selected");
list.appendChild(item);
}
list.appendChild(liButton("pagebutton -more", "…", { role: "more" }));
const last = liButton("pagebutton", String(totalPages), { page: totalPages });
if (currentPage === totalPages) last.classList.add("selected");
list.appendChild(last);
}
}
// Next
const next = liButton("pagebutton -ctrl", nextIcon, { role: "next" });
if (currentPage === totalPages) next.classList.add("disabled");
list.appendChild(next);
}
list.addEventListener("click", (e) => {
const li = e.target.closest(".pagebutton");
if (!li) return;
if (li.classList.contains("disabled")) return;
if (li.classList.contains("-more")) return;
const role = li.dataset.role;
const page = li.dataset.page ? parseInt(li.dataset.page, 10) : NaN;
if (role === "prev") currentPage = clamp(currentPage - 1, 1, totalPages);
else if (role === "next") currentPage = clamp(currentPage + 1, 1, totalPages);
else if (Number.isFinite(page)) currentPage = clamp(page, 1, totalPages);
render();
});
render();
});