Announcement Banner: Free Shipping Progress Bar


As a part 2 of the previous blog, I thought we could enhance the customer experience further with a progress bar at the top of the store indicating the proximity to the free shipping threshold.

Prefer Video?

If you prefer following along on video enjoy below, otherwise read on.

Creating a global threshold setting

To get started here, we need to make one change from our previous code (which you can get here. Rather than specify the threshold in the liquid section we created, we want it to be set in our global theme settings.

This is done by creating a value in the config/settings_schema.json and config/settings_data.json files. Search for ‘cart’ in these files and add the respective lines:

// config/settings_schema.json
...
{
        "type": "collection",
        "id": "cart_drawer_collection",
        "label": "t:settings_schema.cart.settings.cart_drawer.collection.label",
        "info": "t:settings_schema.cart.settings.cart_drawer.collection.info"
      },
// add here
      {
        "type": "text",
        "id": "free-shipping-countdown", 
        "label": "Free Shipping Threshold",
        "info": "The amount in $ at which you offer free shipping"
      }
// to here
    ]
// config/settings_data.json
{
  "current": {
   ...
    "free-shipping-countdown": "1111", // add here
...
  "presets": {
    "Default": {
...
      "free-shipping-countdown": "", // add here
...

Note that after swapping the value in free-shipping-countdown.liquid where threshold is set to = {{settings.free-shipping-countdown}} this value will now be set in the Customizer -> Theme Settings:

Creating the progress bar web component

Now that we have global access to this threshold value we can get started with another web component. Most themes have an “Announcement Bar” section or something similar that is referenced in the header section. For simplicity this time, I will keep my JavaScript within the ‘announcement-bar.liquid’ file.

Firstly, I am going to create the FreeShippingProgressBar web component and it’s constructor method (with its CSS we’ll need):

 class FreeShippingProgressBar extends HTMLElement {
  constructor() {
        super();
        const shadow = this.attachShadow({ mode: 'open' });

        const style = document.createElement('style');
        style.textContent = `
            body {
                margin: 0;
                padding: 0;
                font-family: Arial, sans-serif;
            }

            .progress-bar-container {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                background-color: #f0f0f0;
                padding: 5px;
                box-sizing: border-box;
            }

            .progress-bar {
                height: 10px;
                background-color: #4caf50;
                width: 0;
                transition: width 0.5s ease;
                border-radius: 30px;
            }

            .progress-text {
                font-size: 14px;
                text-align: center;
                color: #333;
            }
        `;

        // Append the style to the shadow DOM
        shadow.appendChild(style);

        // Create a container div in the shadow DOM
        const container = document.createElement('div');
        container.classList.add('progress-bar-container');

        const progressBarContainer = document.createElement('div')
        progressBarContainer.classList.add('progress-bar-container-div')

        // Create the progress bar and progress text elements
        const progressBar = document.createElement('div');
        progressBar.id = 'progress-bar';
        progressBar.classList.add('progress-bar');

        const progressText = document.createElement('div');
        progressText.id = 'progress-text';
        progressText.classList.add('progress-text');

        // Append the progress bar and progress text to the container
        progressBarContainer.appendChild(progressText);
        progressBarContainer.appendChild(progressBar);

        container.appendChild(progressBarContainer)

        // Append the container to the shadow DOM
        shadow.appendChild(container);
    }
}

Now let’s create our connectedCallback method that will kick things in motion for us by calling our method to setup our event listeners and call the initial run through of the ‘updateProgressBar’ function:

    async updateProgressBar(cartTotal, threshold, initial=false) {
        // Assume you have a way to get the current cart total from Shopify
        // const cartTotal = this.getCurrentCartTotal();
        const progressBar = this.shadowRoot.getElementById('progress-bar');
        const progressText = this.shadowRoot.getElementById('progress-text');
        if (!threshold) {
          progressBar.remove()
          progressText.remove()
          return
        }

        if (initial) {
          cartTotal = await this.getShopifyCart()
        }

        if (!cartTotal || cartTotal == 0) {
          progressText.innerHTML = `Free Shipping on Orders Over $${threshold}!`;
          progressBar.style.width = '0%'
          return
        }

        if (cartTotal >= threshold) {
            progressBar.style.width = '100%';
            progressText.innerHTML = 'Congratulations, you qualify for free shipping!';
        } else {
            const progressPercentage = (cartTotal / threshold) * 100;
            progressBar.style.width = `${progressPercentage}%`;
            progressText.innerHTML = `Only $${(threshold - cartTotal).toFixed(2)} away from free shipping`;
        }
    }

    initEventListeners() {
      document.addEventListener('cartUpdated:results', (e) => {
            const cartTotal = e.detail.cartTotal || 0
            const threshold = {{ settings.free-shipping-countdown  }} || 0
            this.updateProgressBar(cartTotal, threshold);
        });
    }

    connectedCallback() {
        const threshold = {{ settings.free-shipping-countdown }} || 0
        this.updateProgressBar(0, threshold, true);
        this.initEventListeners();
    }

In the ‘connectedCallback’ method we are grabbing the globally available threshold from liquid, and setting our progress bar.

While the progress bar will be set the first time in the ‘connectedCallback’ method, after we will use this event listener to reload the component on cart updates. We will also be setting up event emitters at the end, in the ‘free-shipping-countdown.liquid’ file we created in our previous blog. This way we’ll know when the cart is updated and it will run in sync with our slideout cart logic already created.

We also will need to have a function that on first init we can get the cart total. After this initial loading of the component, we will pass that value from the event emitters aforementioned. We can copy this function over directly from our free-shipping-countdown.liquid’:

...
    async getShopifyCart() {
      try {
        const response = await fetch('/cart.js', {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
          }
        });

        if (!response.ok) {
          throw new Error('Network response was not ok');
        }

        const cartData = await response.json();
        return cartData.total_price / 100;
      } catch (error) {
        console.error('Error fetching Shopify cart:', error);
      }
    }
...

And lastly define our component at the bottom.

...
customElements.define('free-shipping-progress-bar', FreeShippingProgressBar);

See the full file here.

Creating a custom cart event

Now we can go to our ‘free-shipping-countdown.liquid’ file we created in the previous blog and emit an event in the ‘updateFreeShippingCountdown’ function like so:

async updateFreeShippingCountdown() {
...
      const cartEvent = new CustomEvent('cartUpdated:results', {'detail': {
        cartTotal: cartTotal.toFixed(2),
        threshold: threshold
      }});
      document.dispatchEvent(cartEvent);
...

Now all we need to do is reference the web component in the liquid code above and voila, we have a fully functioning progress indicator in our announcement banner.

...
           <div class="page-width">
                <p class="announcement-bar__message {{ block.settings.text_alignment }} h5">
                  <span>{{ block.settings.text | escape }}</span>
                  {%- if block.settings.link != blank -%}
                    {% render 'icon-arrow' %}
                  {%- endif -%}
                  </p>
                  <free-shipping-progress-bar></free-shipping-progress-bar> <!-- insert here -->
              </div>

I hope you enjoyed this tutorial creating a free shipping progress indicator. There are so many ways to customize and improve this concept and I would love to here your thoughts and ideas. Thanks for reading!