toolshed
Pagination for long lists
Long lists use pagination, not infinite scroll. Same mechanism as the homepage stream.
🌱
🌿
🪴
🌳
Any page with a long list uses pagination. Not infinite scroll, not “load more.” Previous/next buttons, a page indicator, scroll to top on navigate.
The structure
<nav class="pagination" id="pagination" aria-label="Pages" hidden>
<button class="page-btn" id="page-prev" aria-label="Previous page">Previous</button>
<span class="page-indicator" id="page-indicator"></span>
<button class="page-btn" id="page-next" aria-label="Next page">Next</button>
</nav>
The JS pattern
const PAGE_SIZE = window.innerWidth <= 800 ? 6 : 12;
let currentPage = 1;
function render() {
const matching = allItems.filter(matchesFilters);
const totalPages = Math.max(1, Math.ceil(matching.length / PAGE_SIZE));
if (currentPage > totalPages) currentPage = totalPages;
const start = (currentPage - 1) * PAGE_SIZE;
allItems.forEach(el => { el.style.display = 'none'; });
matching.slice(start, start + PAGE_SIZE).forEach(el => { el.style.display = ''; });
pagination.hidden = totalPages <= 1;
indicator.textContent = `Page ${currentPage} of ${totalPages}`;
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages;
}
prevBtn.addEventListener('click', () => {
if (currentPage > 1) { currentPage--; render(); window.scrollTo({ top: 0, behavior: 'smooth' }); }
});
nextBtn.addEventListener('click', () => {
currentPage++; render(); window.scrollTo({ top: 0, behavior: 'smooth' });
});
Rules
- Filters always reset
currentPage = 1 - Prev/next always scroll to the top of the page
- Hide the nav entirely when everything fits on one page
- PAGE_SIZE: 6 mobile / 12 desktop for dense grids; 6 mobile / 10 desktop for stream cards
- Wrap everything in an
init()function; call it on load and onastro:after-swap
Where used
Homepage stream, library page.