Skip to content

Load More

Total CMS provides “load more” helpers that render the first page of results server-side, then fetch subsequent pages via API as the user scrolls or clicks a button.

  • Edition: Standard edition or higher (requires templates feature)
  • Template file: A Twig template that renders a single item (receives an {{ object }} variable)
  • HTMX: Include the HTMX script in your page or load from a CDN
<script src="{{ cms.api }}/assets/htmx.min.js?v={{ cms.version }}"></script>

Use cms.render.loadMore() to paginate objects from a collection:

{{ cms.render.loadMore('blog', {
template: 'blog/card.twig',
limit: 10
}) }}

This renders the first 10 blog objects using blog/card.twig, then appends an HTMX trigger that automatically fetches the next page when needed.

Use cms.render.loadMoreDataView() to paginate results from a saved DataView:

{{ cms.render.loadMoreDataView('recent-posts', {
template: 'blog/card.twig',
limit: 10
}) }}

Works identically to the collection version but queries a DataView by its ID.

OptionTypeDefaultDescription
templatestringrequiredTwig template file for rendering each item. Receives {{ object }}
limitint20Number of items per page
sortstringSort field (e.g., date, -date for descending)
includestringInclude filter (e.g., published:true,featured:true)
excludestringExclude filter (e.g., draft:true)
searchstringSearch query string
triggerstring'revealed'HTMX trigger mode: revealed or click
labelstring'Load More'Button label (only used when trigger is click)
classstringAdditional CSS class for the trigger element
transitionboolfalseEnable HTMX view transitions

The default mode. A hidden <div> is placed after the rendered items. When it scrolls into view, HTMX automatically fetches the next page.

{{ cms.render.loadMore('blog', {
template: 'blog/card.twig',
trigger: 'revealed'
}) }}

Renders a <button> that the user clicks to load additional items.

{{ cms.render.loadMore('blog', {
template: 'blog/card.twig',
trigger: 'click',
label: 'Show More Posts'
}) }}
  1. Initial render: The first page of items is rendered server-side into the page HTML
  2. HTMX trigger: After the last item, a trigger element is injected (a <div> for infinite scroll or a <button> for click)
  3. API request: When triggered, HTMX sends a GET request to the query endpoint with offset and limit parameters
  4. Response: The server returns the next batch of rendered HTML plus a new trigger element for the following page
  5. Swap: HTMX swaps the trigger element with the new content (items + next trigger) using outerHTML
  6. Chain continues: This repeats until no more items remain, at which point no trigger is returned

Create a Twig template that renders a single item. The template receives the current object as {{ object }}:

{# templates/blog/card.twig #}
<article class="blog-card">
<h2><a href="{{ cms.collection.objectUrl('blog', object.id) }}">{{ object.title }}</a></h2>
<time>{{ object.date }}</time>
<p>{{ object.excerpt }}</p>
</article>

The same template is used for both the initial server render and all subsequent HTMX-loaded pages.

The trigger element uses the cms-load-more CSS class, which you can target for custom styling:

.cms-load-more {
text-align: center;
padding: 2rem 0;
}
/* Style the load more button */
button.cms-load-more {
background: #333;
color: #fff;
padding: 0.75rem 2rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
<div class="blog-feed">
{{ cms.render.loadMore('blog', {
template: 'blog/card.twig',
limit: 12,
sort: '-date',
include: 'published:true',
exclude: 'draft:true'
}) }}
</div>
<div class="product-grid">
{{ cms.render.loadMore('products', {
template: 'products/tile.twig',
limit: 24,
trigger: 'click',
label: 'Load More Products',
include: 'instock:true'
}) }}
</div>
<div class="recent-activity">
<h2>Recent Activity</h2>
{{ cms.render.loadMoreDataView('recent-activity', {
template: 'dashboard/activity-row.twig',
limit: 20,
trigger: 'revealed'
}) }}
</div>