All Posts
Published: November 1, 2025

When building dynamic layouts in Themeco’s Cornerstone, it’s common to display posts in a loop—whether it’s blog articles, case studies, or portfolio items. But native pagination can interrupt the browsing flow. The script below replaces traditional pagination with an AJAX-powered “Load More” button that appends new posts inline without reloading the page.
How It Works
This lightweight JavaScript detects .load-more containers inside your Cornerstone layout and pairs them with a .load-more__btn element directly after each container. When the button is clicked, the script:
- Builds the Next Page URL
It calculates the next page number (?paged=2,?paged=3, etc.) and fetches that URL asynchronously. - Parses and Extracts Posts
The script usesDOMParserto grab the next batch of.x-row-innerchildren (or direct child nodes if none exist). - Appends Content Seamlessly
All new posts are appended to the current container without breaking Cornerstone’s flexbox or grid structure. - Manages Button States
The button dynamically updates its label toLoading…, restores its original text after completion, and hides itself once no more posts are available. - Plays Nicely With Cornerstone Styling
It respects Cornerstone’s anchor/text classes (.x-anchor-text,.x-text-primary, etc.), ensuring your “Load More” button inherits native design tokens.
Basic Setup
- Wrap your loop in a container with the class
.load-moreand (optionally)data-per-page="20". - Place a button with the class
.load-more__btnimmediately after the container. - To edit the “Loading…” text simply look for loadingLabel in the Javascript below and edit the text.
(function(){
function getTarget(container){
const inner = container.querySelector(':scope > .x-row-inner');
return inner || container;
}
function getNextNodes(doc){
const otherContainer = doc.querySelector('.load-more');
if (!otherContainer) return [];
const otherInner = otherContainer.querySelector(':scope > .x-row-inner');
const source = otherInner || otherContainer;
return Array.from(source.children);
}
function nextPageUrl(baseHref, nextPage){
const clean = baseHref.split('#')[0];
const u = new URL(clean);
u.searchParams.set('paged', String(nextPage));
return u.toString();
}
// Prefer a "primary" label; otherwise fall back to any anchor-text
function getButtonTextEl(btn){
return (
btn.querySelector('.x-anchor-text.x-text-primary') ||
btn.querySelector('.x-anchor-text-primary') ||
btn.querySelector('.x-anchor-text') ||
btn.querySelector('.x-anchor-label, .x-button-text, .x-text') ||
btn.querySelector('.x-anchor-content span, .x-anchor-content div')
);
}
const containers = Array.from(document.querySelectorAll('.load-more'));
containers.forEach((container) => {
let button = container.nextElementSibling;
if (!button || !button.classList.contains('load-more__btn')) {
button = container.parentElement && container.parentElement.querySelector('.load-more__btn');
}
if (!button) return;
const target = getTarget(container);
let page = (() => {
const p = new URL(window.location.href).searchParams.get('paged');
const n = parseInt(p || '1', 10);
return Number.isFinite(n) && n > 0 ? n : 1;
})();
const perPageAttr = container.getAttribute('data-per-page');
const perPage = perPageAttr ? parseInt(perPageAttr, 10) : 20;
if (!button.dataset.loadingLabel) button.dataset.loadingLabel = 'Loading…';
const textEl = getButtonTextEl(button);
if (textEl) {
if (!button.dataset.originalLabel) button.dataset.originalLabel = textEl.textContent.trim();
if (!button.dataset.originalClass) button.dataset.originalClass = textEl.className; // preserve classes
}
let loading = false;
async function loadMore(){
if (loading) return;
loading = true;
// clean up any aria-busy to avoid X padding/spinner side effects
button.removeAttribute('aria-busy');
button.classList.add('is-loading');
button.setAttribute('disabled','disabled');
// Swap text but keep markup; optionally ensure primary styling during load
if (textEl) {
textEl.textContent = button.dataset.loadingLabel;
if (!textEl.classList.contains('x-text-primary')) {
textEl.classList.add('x-text-primary'); // temporary to keep look consistent
}
}
try {
const url = nextPageUrl(window.location.href, page + 1);
const res = await fetch(url, { credentials: 'same-origin' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const nodes = getNextNodes(doc);
if (!nodes.length) {
button.style.display = 'none';
return;
}
const frag = document.createDocumentFragment();
nodes.forEach(n => frag.appendChild(n));
target.appendChild(frag);
document.dispatchEvent(new CustomEvent('loadmore:append', { detail: { container: target, nodes } }));
page += 1;
if (nodes.length < perPage) {
button.style.display = 'none';
}
} catch (err) {
console.error(err);
} finally {
button.classList.remove('is-loading');
button.removeAttribute('disabled');
// restore original label + classes exactly
if (textEl) {
if (button.dataset.originalLabel) textEl.textContent = button.dataset.originalLabel;
if (button.dataset.originalClass) textEl.className = button.dataset.originalClass;
}
loading = false;
}
}
button.addEventListener('click', loadMore);
});
})();