When building modern web applications, handling large datasets efficiently is a must-have skill. If you try to load 1,000 items at once, your page will lag, and your users will leave. The solution is Server-Side Pagination.
In this tutorial, we’ll build a robust pagination system that fetches data from a live API, handles dynamic button generation, and includes “smart” ellipsis (...) for a professional UI.
1. The Strategy: “The Sliding Window”
Instead of showing every page button, we use a “Sliding Window” logic. This keeps the UI clean by only showing:
-
The First and Last pages.
-
A small Range (2 pages) around the current page.
-
Ellipses (…) to bridge the gaps.
2. The Full Implementation
Here is the complete, single-file solution. You can save this as index.html and run it immediately.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Professional API Pagination</title>
<style>
:root { --primary: #007bff; --bg: #f4f7f6; --text: #333; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: var(--bg); color: var(--text); padding: 40px 20px; max-width: 700px; margin: auto; }
h2 { text-align: center; color: #444; }
/* List Styling */
#list { list-style: none; padding: 0; min-height: 400px; }
.post-item { background: #fff; border: 1px solid #ddd; margin: 10px 0; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); transition: transform 0.2s; }
.post-item:hover { transform: translateY(-2px); }
.post-item h3 { margin: 0 0 8px 0; font-size: 1.1rem; color: var(--primary); }
/* Controls Styling */
.controls { display: flex; gap: 8px; justify-content: center; align-items: center; margin-top: 30px; flex-wrap: wrap; }
button { padding: 8px 16px; border: 1px solid var(--primary); background: #fff; color: var(--primary); cursor: pointer; border-radius: 4px; font-weight: 600; }
button:hover:not(:disabled) { background: var(--primary); color: white; }
button.active { background: var(--primary); color: white; }
button:disabled { border-color: #ccc; color: #ccc; cursor: not-allowed; }
.dots { color: #888; padding: 0 5px; font-weight: bold; }
.loading { text-align: center; font-weight: bold; color: var(--primary); display: none; }
</style>
</head>
<body>
<h2>Published Posts</h2>
<div id="loading" class="loading">Fetching Data...</div>
<ul id="list"></ul>
<div class="controls">
<button id="prevBtn">Previous</button>
<div id="page-numbers"></div>
<button id="nextBtn">Next</button>
</div>
<script>
// State Management
let currentPage = 1;
const itemsPerPage = 6;
let totalPages = 0;
// DOM Elements
const listEl = document.getElementById('list');
const pageNumbersEl = document.getElementById('page-numbers');
const loadingEl = document.getElementById('loading');
/**
* Fetches data from API using page and limit parameters
*/
async function fetchPosts(page) {
loadingEl.style.display = 'block';
listEl.style.opacity = '0.5';
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${itemsPerPage}`);
const data = await response.json();
// Calculate total pages from the API header
const totalCount = response.headers.get('x-total-count');
totalPages = Math.ceil(totalCount / itemsPerPage);
renderPosts(data);
renderButtons();
} catch (error) {
listEl.innerHTML = `<p style="color:red">Error loading data. Please try again.</p>`;
} finally {
loadingEl.style.display = 'none';
listEl.style.opacity = '1';
}
}
/**
* Renders the list of posts
*/
function renderPosts(posts) {
listEl.innerHTML = posts.map(post => `
<li class="post-item">
<h3>${post.id}. ${post.title.substring(0, 40)}...</h3>
<p>${post.body.substring(0, 80)}...</p>
</li>
`).join('');
}
/**
* Renders the smart pagination buttons with ellipses
*/
function renderButtons() {
pageNumbersEl.innerHTML = '';
const range = 1; // How many pages to show around the current page
let lastRenderedPage = 0;
for (let i = 1; i <= totalPages; i++) {
// Logic: Always show first, last, and the range around current page
const isFirstOrLast = i === 1 || i === totalPages;
const isWithinRange = i >= currentPage - range && i <= currentPage + range;
if (isFirstOrLast || isWithinRange) {
// If there's a gap, add dots
if (lastRenderedPage && i - lastRenderedPage > 1) {
const dots = document.createElement('span');
dots.innerText = '...';
dots.className = 'dots';
pageNumbersEl.appendChild(dots);
}
const btn = document.createElement('button');
btn.innerText = i;
btn.className = i === currentPage ? 'active' : '';
btn.onclick = () => {
currentPage = i;
fetchPosts(i);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
pageNumbersEl.appendChild(btn);
lastRenderedPage = i;
}
}
// Update disabled state for arrows
document.getElementById('prevBtn').disabled = currentPage === 1;
document.getElementById('nextBtn').disabled = currentPage === totalPages;
}
// Arrow Button Listeners
document.getElementById('prevBtn').onclick = () => {
if (currentPage > 1) fetchPosts(--currentPage);
};
document.getElementById('nextBtn').onclick = () => {
if (currentPage < totalPages) fetchPosts(++currentPage);
};
// Initial Load
fetchPosts(currentPage);
</script>
</body>
</html>
3. How the “Smart Dots” Logic Works
The magic happens in the renderButtons function. We track the lastRenderedPage. If the current page i we are about to draw is more than 1 step away from the lastRenderedPage, we know there is a gap.
For example, if we just drew page 1 and the next page the logic allows is page 8, the script sees that 8 - 1 > 1 and injects the ... span automatically.
4. Key Takeaways
-
Performance: We only fetch 6 items at a time, making the app incredibly light.
-
Scalability: This code works just as well for 1,000 pages as it does for 5.
-
User Experience: We included
window.scrollToso that when a user clicks a page, they are taken back to the top of the list.
Useful links below:
Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service
Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2
Best email marketing automation solution on the market! http://www.aweber.com/?373860
Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29
Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff
Buy me a coffee https://buymeacoffee.com/tyronneratcliff



