What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
To add infinite scroll to WordPress, choose the implementation that matches your theme. A classic theme already using Jetpack can enable Jetpack Infinite Scroll with a few lines of PHP. A block theme or custom front end should use the WordPress REST API and JavaScript. If your archive is rendered in PHP and needs custom queries or filters, use AJAX with WP_Query.
Antihost offers managed WordPress hosting with free migration, one-click staging and a 14-day free trial.
Whichever route you choose, load bounded batches, expose a clear end state, keep a visible “Load more” or ordinary-pagination fallback, and preserve each post’s normal permalink. Infinite scroll should extend an archive—not make the footer, navigation, accessibility controls, or individual post URLs harder to use.
As an Amazon Associate I earn from qualifying purchases.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Choose an implementation before you write code
Your theme type and rendering model determine the simplest reliable solution.
| Situation | Best starting point | What you control | Main limitation |
|---|---|---|---|
| Classic theme already running Jetpack | Jetpack Infinite Scroll | Automatic scrolling or click-to-load, container and footer behavior | Jetpack’s documented support is for classic themes, not block themes |
| Block theme or JavaScript-driven custom front end | WordPress REST API plus JavaScript | Markup, filters, loading rules, analytics and interaction design | You must implement loading, errors, accessibility and the end state |
| PHP-rendered archive with a custom query | AJAX endpoint backed by WP_Query |
Server-side templates, permissions, taxonomy filters and query logic | You must keep paging and the initial query exactly in sync |
Infinite scroll is a presentation pattern, not a replacement for pagination. Keep an ordinary archive URL and a direct permalink for every post. Search engines, keyboard users and people who open a result in a new tab should not depend on a JavaScript session to find content.
#1 Best Overall
Method 1: Enable Jetpack Infinite Scroll in a classic theme
Jetpack describes Infinite Scroll as automatically loading more content as a visitor scrolls. It is provided free to sites running Jetpack. Its documented theme-support API applies to classic themes.
1. Confirm the theme and container
Find the element that wraps the archive’s post cards. Its HTML id must match the container value in the theme-support declaration. For example, an archive might contain:
<main id="content">
<!-- the archive loop outputs posts here -->
</main>
Do not use a class name in place of the ID. Jetpack inserts subsequent posts into this element.
2. Add theme support
Put this in the classic theme’s functions.php (preferably in a child theme or a site-specific plugin so a theme update does not remove it):
function mytheme_infinite_scroll_init() {
add_theme_support('infinite-scroll', array(
'container' => 'content',
'type' => 'click', // or 'scroll'
'footer' => 'page',
));
}
add_action('after_setup_theme', 'mytheme_infinite_scroll_init');
Replace content with the real ID from your archive. The type value selects the interaction: scroll requests another batch automatically, while click displays a control that the visitor activates.
Rank #2
3. Choose scroll or click mode deliberately
- Automatic scroll: convenient for a feed, but it can make a footer difficult to reach and can trigger requests before a visitor has decided to continue.
- Click-to-load: gives the visitor control, exposes a reliable keyboard target, and keeps footer links reachable. It is usually the safer default for archives, guides and media libraries.
Jetpack documents that footer widgets can cause the experience to switch to click mode so the footer remains accessible. A fixed-footer option is also documented for designs that need a persistent footer treatment. When all posts have loaded, Jetpack adds the infinity-end class to the body, which you can use to hide loading UI or style the completed state.
Recommended Free Tools
4. Test the loop and the end state
- Open an archive with more than one page of posts.
- Activate the load control with a mouse and with the keyboard.
- Confirm that the next batch appears inside the intended container and uses the same card markup as the first batch.
- Continue until the final post. Verify that the control disappears or changes to a clear “All posts loaded” state.
- Check an archive with a footer widget. Confirm that footer links remain reachable.
- Test a narrow viewport and a slow connection; cards should not jump over the loading control or overlap it.
Method 2: Build infinite scroll with the WordPress REST API
The REST API exposes the posts collection at /wp-json/wp/v2/posts. You request a page of records, append the returned JSON to the archive, and stop when the response reports the final page. WordPress permits per_page values from 1 through 100. Responses include X-WP-Total and X-WP-TotalPages headers.
1. Decide the batch and initial page
If the server rendered page 1 already contains ten cards, set page to 1 in JavaScript and request page 2 first. Keep the same ordering and filters as the initial archive. A request for ten posts looks like:
/wp-json/wp/v2/posts?page=2&per_page=10&_embed
_embed asks WordPress to include related data such as featured media in the response, which can avoid a second request for each card. Lower batch sizes reduce the amount of DOM work per interaction; higher values mean fewer requests. Do not exceed 100, the documented REST limit.
2. Mark up a list, sentinel, fallback button and status
<div id="post-list">
<!-- page 1 cards are rendered here -->
</div>
<div id="load-sentinel" aria-hidden="true"></div>
<button id="load-more" type="button">Load more posts</button>
<p id="load-status" aria-live="polite"></p>
The sentinel lets an IntersectionObserver start a request near the end of the list. Keep the button visible or reveal it when automatic loading fails; it is the keyboard and no-observer fallback.
Rank #3
3. Fetch, append and stop at the reported final page
let page = 1;
const perPage = 10;
let loading = false;
let finished = false;
const list = document.querySelector('#post-list');
const sentinel = document.querySelector('#load-sentinel');
const button = document.querySelector('#load-more');
const status = document.querySelector('#load-status');
function appendPostCard(post) {
const article = document.createElement('article');
article.className = 'post-card';
const title = document.createElement('h2');
const link = document.createElement('a');
link.href = post.link;
link.textContent = post.title.rendered;
title.append(link);
article.append(title);
list.append(article);
}
async function loadNextBatch() {
if (loading || finished) return;
loading = true;
button.disabled = true;
status.textContent = 'Loading more posts…';
const nextPage = page + 1;
const response = await fetch(
`/wp-json/wp/v2/posts?page=${nextPage}&per_page=${perPage}&_embed`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const posts = await response.json();
posts.forEach(appendPostCard);
page = nextPage;
const totalPages = Number(
response.headers.get('X-WP-TotalPages') || page
);
if (page >= totalPages || posts.length === 0) {
finished = true;
observer.disconnect();
button.hidden = true;
status.textContent = 'All posts loaded.';
} else {
status.textContent = '';
button.disabled = false;
}
}
const observer = new IntersectionObserver((entries) => {
if (entries.some(entry => entry.isIntersecting)) {
loadNextBatch().catch(() => {
loading = false;
button.disabled = false;
button.hidden = false;
status.textContent = 'Could not load more posts. Use the button to try again.';
});
}
});
observer.observe(sentinel);
button.addEventListener('click', () => {
loadNextBatch().catch(() => {
loading = false;
button.disabled = false;
status.textContent = 'Could not load more posts. Please try again.';
});
});
The example keeps a loading lock so two observer callbacks cannot request the same page. It reads X-WP-TotalPages and disconnects the observer at the end. The post.link value remains the canonical, shareable URL for each item.
4. Match the archive’s real query
The plain posts endpoint returns the site’s post collection. Category, tag, author, search and custom post-type archives need the corresponding REST parameters or route. For example, a category archive must pass the category identifier while preserving the same ordering used for page 1. If your front end applies a custom filter that the endpoint does not understand, create a purpose-built REST route rather than silently loading unrelated posts.
5. Handle authentication boundaries
Public content is generally available without authentication. Private, protected or permission-filtered content retains WordPress’s authentication restrictions. Do not put privileged tokens in browser JavaScript. If a response is a 401 or 403, show an explanatory state and preserve the normal archive navigation.
Method 3: Add a PHP/AJAX loop with WP_Query
Use this approach when the server already owns the archive markup, when a complex query must remain in PHP, or when the same template part must serve both the first page and later batches.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitches1. Keep the initial query and later pages consistent
$paged = max(1, (int) get_query_var('paged'));
$query = new WP_Query(array(
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => $paged,
));
posts_per_page sets the batch size and paged selects the page. Do not use a different batch size in the AJAX request: changing it shifts the offset and can duplicate or skip posts. Leave nopaging disabled so WordPress can calculate the number of pages.
Rank #4
2. Reuse the archive’s template part
Render each result with the same template part used by the initial loop. That preserves headings, featured images, metadata, structured data and responsive classes. The AJAX response should contain only the new cards, not a second document shell. Your browser script can request the next page number, insert the returned HTML after the existing cards, and stop when the server reports no more pages.
3. Secure and validate the handler
- Register the AJAX action for the audiences that need it; do not expose privileged data to unauthenticated visitors.
- Validate and clamp the incoming page number and any taxonomy or search arguments.
- Use a nonce where the action or content requires request verification.
- Escape output in the template and return a clear JSON or HTML error when the query fails.
- Return an explicit
has_morevalue or total-page count so the browser can remove its loading control.
The browser-side behavior should still include a visible button, a loading lock, an error message and a final state. AJAX changes the transport; it does not remove the accessibility and navigation responsibilities of infinite scroll.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Accessibility, navigation and SEO requirements
Keep a reachable footer
Automatic loading can continually push a footer downward. Click-to-load, a fixed footer treatment, or a deliberate end marker gives visitors a reliable way to reach navigation, contact links and legal information.
Provide status feedback
Use an aria-live="polite" region for “Loading,” errors and completion. Disable the button while a request is in flight. Do not move keyboard focus unexpectedly after every batch; let a visitor continue from the control they activated. If focus management is needed for a major state change, move it intentionally and document that behavior.
Preserve ordinary URLs
Every appended card should contain its normal post permalink. Keep canonical archive and pagination URLs available even when JavaScript is enabled. Verify the result with the site’s existing SEO configuration rather than assuming that an infinite feed alone creates indexable pages.
Best Value
Make failure non-destructive
A timeout, a blocked script, an API permission error or an ad-blocking rule should not erase page 1. Leave the initial server-rendered posts in place and expose ordinary pagination or a retry button.
Performance and request design
- Use a bounded batch that fits the card layout; the REST API’s maximum is 100 records, but the maximum is rarely the right visual batch.
- Request only the fields and related data you need. Embedded media can reduce follow-up requests but increases each response.
- Trigger the observer near the list end, not on every scroll event.
- Prevent duplicate requests with a loading flag and update it in both success and failure paths.
- Stop immediately when the API’s total-page header, an explicit AJAX flag, or an empty result says there is no next batch.
- Measure layout stability on slow connections so images reserve space and newly inserted cards do not shift the user’s reading position.
Troubleshooting common failures
Nothing loads after the first page
For Jetpack, check that the container ID exactly matches the container setting and that the code runs on after_setup_theme. For REST, inspect the request URL and response status, then verify that the initial page and requested page use the same filters. For AJAX, confirm that the handler is registered and that the page parameter reaches WP_Query.
Posts are duplicated or skipped
Usually the client increments the page twice, two observer callbacks run together, or the server uses a different posts_per_page from page 1. Keep one authoritative page variable, lock while loading, and retain identical ordering and filters.
The footer cannot be reached
Switch Jetpack to click mode, add a persistent fallback button, or use an explicit end marker. Check whether footer widgets are changing the interaction mode and test the page with keyboard navigation.
The browser reports a REST 400, 401 or 403
A 400 commonly indicates an invalid page, filter or parameter value. A 401 or 403 indicates authentication or permission restrictions. Display the normal archive fallback instead of repeatedly retrying a request the visitor cannot authorize.
The final loading control remains visible
Use the API’s X-WP-TotalPages header or the server’s explicit total-page value. Also treat an empty array as the end. In Jetpack, the documented infinity-end body class can drive final-state styling.
Which method should you use?
- Choose Jetpack when you have a classic theme, already run Jetpack, and want the quickest supported setup with minimal custom JavaScript.
- Choose the REST API for a block theme, headless front end or custom card interface where the browser owns rendering and interaction.
- Choose PHP/AJAX when archive logic, permissions and template rendering belong on the server and must stay aligned with WordPress queries.
Start with click-to-load unless your feed is intentionally immersive and you have tested footer access, keyboard operation, error recovery and direct post URLs. Automatic loading is an option; a recoverable, navigable archive is the requirement.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




