NorthJS

Template

Cart drawer notice

A free-shipping progress notice for the cart drawer, driven by cart totals and updated on every cart change.

Renders the amount remaining before free shipping, plus a progress bar. Values come from the cart object, so the notice is correct on first paint and does not flash a placeholder.

Set your threshold in the snippet call — `{% render 'cart-notice', threshold: 7500 %}` — using the amount in cents, matching Shopify money fields.

Code

Copy a single file, or download all 3 as an archive. Duplicate your theme before pasting into a live store.

  • template.liquidLiquid
    {% comment %}
      Cart drawer notice — free shipping progress.
      Usage: {% render 'cart-notice', threshold: 7500 %}
    {% endcomment %}
    
    {%- liquid
      assign goal = threshold | default: 7500
      assign remaining = goal | minus: cart.total_price
      assign progress = cart.total_price | times: 100.0 | divided_by: goal
      if progress > 100
        assign progress = 100
      endif
    -%}
    
    <div class="cart-notice" data-cart-notice data-threshold="{{ goal }}">
      {%- if remaining > 0 -%}
        <p class="cart-notice__text">
          You are {{ remaining | money }} away from free shipping.
        </p>
      {%- else -%}
        <p class="cart-notice__text cart-notice__text--met">
          Your order qualifies for free shipping.
        </p>
      {%- endif -%}
    
      <div
        class="cart-notice__track"
        role="progressbar"
        aria-valuemin="0"
        aria-valuemax="100"
        aria-valuenow="{{ progress | round }}"
        aria-label="Progress toward free shipping"
      >
        <div class="cart-notice__bar" style="--cart-notice-progress: {{ progress }}%"></div>
      </div>
    </div>

  • template.cssCSS
    .cart-notice {
      display: grid;
      gap: 0.5rem;
      padding: 0.875rem 1rem;
      background: rgb(0 0 0 / 0.04);
    }
    
    .cart-notice__text {
      margin: 0;
      font-size: 0.875rem;
    }
    
    .cart-notice__text--met {
      font-weight: 600;
    }
    
    .cart-notice__track {
      height: 0.375rem;
      background: rgb(0 0 0 / 0.1);
      overflow: hidden;
    }
    
    .cart-notice__bar {
      height: 100%;
      width: var(--cart-notice-progress, 0%);
      background: currentColor;
      transition: width 200ms ease-out;
    }
    
    @media (prefers-reduced-motion: reduce) {
      .cart-notice__bar {
        transition: none;
      }
    }

  • template.jsJavaScript
    (() => {
      const formatter = new Intl.NumberFormat(document.documentElement.lang || 'en', {
        style: 'currency',
        currency: window.Shopify?.currency?.active || 'USD',
      });
    
      async function refresh() {
        const notice = document.querySelector('[data-cart-notice]');
        if (!notice) return;
    
        const threshold = Number(notice.dataset.threshold || 0);
        if (!threshold) return;
    
        const response = await fetch(window.Shopify.routes.root + 'cart.js');
        if (!response.ok) return;
    
        const cart = await response.json();
        const remaining = threshold - cart.total_price;
        const progress = Math.min((cart.total_price / threshold) * 100, 100);
    
        const text = notice.querySelector('.cart-notice__text');
        const bar = notice.querySelector('.cart-notice__bar');
        const track = notice.querySelector('.cart-notice__track');
    
        if (text) {
          text.textContent =
            remaining > 0
              ? `You are ${formatter.format(remaining / 100)} away from free shipping.`
              : 'Your order qualifies for free shipping.';
          text.classList.toggle('cart-notice__text--met', remaining <= 0);
        }
        if (bar) bar.style.setProperty('--cart-notice-progress', `${progress}%`);
        if (track) track.setAttribute('aria-valuenow', String(Math.round(progress)));
      }
    
      document.addEventListener('cart:updated', refresh);
      document.addEventListener('shopify:section:load', refresh);
    })();