Skip to Main Content
Sprig hero

Reactive Pagination With Sprig and Craft

The Sprig plugin for Craft CMS is a relatively new plugin by well established plugin developer Ben Croker. Sprig makes it easy to create reactive components using twig that can re-render themselves on user triggered events.

This makes it great for things like Pagination and search - instead of having the entire page reload on clicking a pagination link or submitting a search form, that section of the page re-renders itself. It's very slick and also makes the website feel faster and more like an app.

I have used this on three client sites setting up search results that update on changing a drop down select, typing into a search field, loading more entries on a button click, and pagination.

I also implemented pagination on the blog that you are reading right now. If you go to the blog landing page and click a pagination link at the bottom you will see the results change but the page did not reload.

Reactive Pagination

Using sprig is very simple after installing the plugin, move to your template and take the part that you want to be reactive and place it into a component. Ben suggests creating a _components directory and placing all components there. I placed mine in blog/_components.

Now you include that component using a sprig tag like this. Notice that we're passing in the entry limit here. { 'limit':6}.

    
    
      <section class="bg-white py-16">
      {# make blog pagination reactive with sprig #}
    {{ sprig('blog/_components/blog-listing', { 'limit':6}) }}
  </section>
    
  

Sprigify the Component

Next we're going to make the component reactive. Since this is pagination we need to set some variables at the top of the page.

    
    
      {# set variable for sprig to hook into with default for first page load #}
  {% set page = page ?? 1 %}

  {# need above values in hidden inputs for persistent state  #}
  {{ hiddenInput('page', page) }}
    
  

Output the Entries

Now to get all the entries with any parameters we want and the current page as well as setup the pagination

    
    
      {% set params = { section: 'blog', limit: limit } %}
    {% if category is defined %}
      {% set params = params | merge({ relatedTo: category }) %}
    {% endif %}

    {% set entryQuery = craft.entries(params) %}

    {# Paginates the entry query given the current page #}
    {% set pageInfo = sprig.paginate(entryQuery, page) %}
    {% set entries = pageInfo.pageResults %}

    {% for entry in entries %}
        {# entry code here #}
    {% endfor %}
    
  

Pagination Code

The key to making things reactive is to add the sprig attribute to any element that you want to cause a re-render of the code. For example if it was a dropdown in form you would have <select sprig name="myselect" id="myselect"> or in this case the pagination links will have the sprig attribute added so that when clicked the list of entries is re-rendered.

The docs for Sprig include an example of a simple list of all pages, but I wanted to show next, previous, first, last, and a limited number of pages around the currently viewed page. Below you can see how that works.

    
    
      {# pagination #}
  <div class="container mx-auto flex flex-wrap flex-col md:flex-row px-4 md:px-0">

    <ul class="flex mx-auto mt-12 md:mt-8 list-reset w-auto font-sans">

      {# set height & width of svg icons here #}
      {% set iconDims = "16" %}

      {% if page > 1 %}
        {# go to first page #}
        <li>
          <a
            sprig
            s-val:page="1"
            class="block hover:bg-grey-lighter text-base text-orange no-underline pl-0 pr-2 py-2 cursor-pointer">
            {{ macros.icon("chevron-double-left", "fill-current inline-block svg-shadow", iconDims, iconDims) }}
          </a>
        </li>

        {# go to previous page #}
        <li>
          <a
            sprig
            s-val:page="{{ page -1 }}"
            class="block hover:bg-grey-lighter text-base text-orange no-underline pl-0 pr-2 py-2 cursor-pointer" >
            {{ macros.icon("chevron-left", "fill-current inline-block svg-shadow", iconDims, iconDims) }}
          </a>
        </li>
      {% endif %}

      {% for i in pageInfo.getDynamicRange(7) %}
        {% if i == page %}
          <li><a class="block bg-grey-lighter text-orange mx-2 px-3 py-2 cursor-pointer">{{ i }}</a></li>
        {% else %}
          <li>
            <a
              sprig
              s-val:page="{{ i }}"
              class="block hover:bg-grey-lighter text-base text-orange no-underline px-4 py-2 cursor-pointer">
              {{ i }}
            </a>
          </li>
        {% endif %}

      {% endfor %}

      {% if page < pageInfo.totalPages %}
        {# go to next page #}
        <li>
          <a
            sprig
            s-val:page="{{ page + 1 }}"
            class="block hover:bg-grey-lighter text-base text-orange no-underline pl-0 pr-2 py-2 cursor-pointer">
            {{ macros.icon("chevron-right", "fill-current inline-block svg-shadow", iconDims, iconDims) }}
          </a>
        </li>

        {# go to last page #}
        <li>
          <a
            sprig
            s-val:page="{{ pageInfo.last }}"
            class="block hover:bg-grey-lighter text-base text-orange no-underline pl-0 pr-2 py-2 cursor-pointer">
            {{ macros.icon("chevron-double-right", "fill-current inline-block svg-shadow", iconDims, iconDims) }}
          </a>
        </li>

      {% endif %}

    </ul>

  </div><!-- /.container mx-auto flex flex-wrap flex-col -->
    
  

After adding all that. When clicking through pagination, the content was reloading but invisible. I looked and I could see that the code had re-rendered correctly in the dom but was not visible.

After investigating I determined that the javascript I was using for the onscroll animations was not working on re-render. There is a simple fix for this though and that is to run the function after swap using this code from the Sprig docs.

I additionally added in a scroll to top of the page so that visitors can see all new entries.

    
    
      /* place this code in the parent template next to the sprig include */

<script>
      htmx.on('htmx:afterSwap', function(event) {
        //reset data-sal so animations work on pagination
        sal({
          reset:true
        });
        // scroll to top so we aren't left at the bottom of the new page
        window.scrollTo(0, 50);
      });
    </script>
    
  

Update the URL Bar

Now that the page is reactive we want the url to be updated so that it is easy to bookmark or share. Probably not as important with pagination, but this is definitely useful when using search.

This can be down with s-push-url, however if your page has multiple parameters that need to be pushed to the url adding the following to your template is a better option.

    
    
      {# push the page number #}
{% do sprig.pushUrl('?' ~ {page: page}|url_encode) %}

{# 
 another site pushes two values 
 page & gridListView
#}
{% do sprig.pushUrl('?' ~ {page: page, gridListView: gridListView}|url_encode) %}
    
  

Related Articles