All Posts

Load More Button on Archives in Cornerstone

Published: November 1, 2025
Share:
Image

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:

  1. Builds the Next Page URL
    It calculates the next page number (?paged=2, ?paged=3, etc.) and fetches that URL asynchronously.
  2. Parses and Extracts Posts
    The script uses DOMParser to grab the next batch of .x-row-inner children (or direct child nodes if none exist).
  3. Appends Content Seamlessly
    All new posts are appended to the current container without breaking Cornerstone’s flexbox or grid structure.
  4. Manages Button States
    The button dynamically updates its label to Loading…, restores its original text after completion, and hides itself once no more posts are available.
  5. 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

  1. Wrap your loop in a container with the class .load-more and (optionally) data-per-page="20".
  2. Place a button with the class .load-more__btn immediately after the container.
  3. 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);
});
})();