Quantity Price-Break Table

Buy 2 save 10%, buy 3 save 15%: a tiered pricing table with the price each worked out.

One section file Product page, near the quantity picker Edit in the theme editor

Important: Before making any changes to your theme, duplicate it in Online Store > Themes so you have a backup.

Volume pricing is easier to sell when shoppers can see it. This table lists your quantity tiers, works out the price each for the selected variant, and highlights the tier that matches the quantity picker. Tap a row to set the quantity.

Preview

What you'll build

Buy more, save more

Quantity Price each You save
1+ £20.00 —
2+ £18.00 10%
3+ Most popular £17.00 15%
5+ £16.00 20%

Discount applied automatically at checkout.

Preview with the default settings. Colours, text and layout can all be changed in the theme editor.

Install

Add it to your store in 4 steps

  1. 1

    Open your theme code

    In Shopify admin go to Online Store > Themes. Next to the theme you want to edit, click the three dots and choose Edit code.

  2. 2

    Create the section

    In the Sections folder click Add a new section, name it lt-quantity-price-breaks, delete the placeholder code, paste in the code below and click Save.

  3. 3

    Add it in the theme editor

    Click Customize, go to Products > Default product, click Add section and choose Quantity price breaks.

  4. 4

    Make it yours

    Change the settings in the sidebar, drag the section where you want it, and click Save.

sections/lt-quantity-price-breaks.liquid · 248 lines
{%- comment -%}
  Quantity price-break table by LAUNCHTIP
  https://launchtip.com/free-shopify-sections-code
  This table only displays the tiers. Create a matching automatic discount
  in Shopify admin > Discounts so the price changes at checkout.
{%- endcomment -%}
{%- liquid
  if product != blank
    assign base_price = product.selected_or_first_available_variant.price
  else
    assign base_price = 0
  endif
-%}

{%- style -%}
  #lt-{{ section.id }} { padding: {{ section.settings.padding }}px 0; }
  #lt-{{ section.id }} .lt-qpb__heading { margin: 0 0 10px; }
  #lt-{{ section.id }} table { width: 100%; border-collapse: collapse; border: 1px solid {{ section.settings.border_color }}; border-radius: 8px; overflow: hidden; }
  #lt-{{ section.id }} th, #lt-{{ section.id }} td { padding: 10px 14px; text-align: left; border-bottom: 1px solid {{ section.settings.border_color }}; }
  #lt-{{ section.id }} th { font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; }
  #lt-{{ section.id }} tbody tr { cursor: pointer; }
  #lt-{{ section.id }} tbody tr.is-active { background: {{ section.settings.highlight }}; }
  #lt-{{ section.id }} .lt-qpb__badge { display: inline-block; margin-left: 6px; padding: 2px 8px; border-radius: 999px; font-size: 0.75em; background: {{ section.settings.badge_color }}; color: {{ section.settings.badge_text }}; }
  #lt-{{ section.id }} .lt-qpb__save { color: {{ section.settings.save_color }}; font-weight: 600; }
  #lt-{{ section.id }} .lt-qpb__note { margin: 8px 0 0; font-size: 0.85em; opacity: 0.7; }
{%- endstyle -%}

<div id="lt-{{ section.id }}" class="page-width" data-currency="{{ cart.currency.iso_code }}">
  {%- if section.settings.heading != blank -%}
    <h2 class="lt-qpb__heading h4">{{ section.settings.heading }}</h2>
  {%- endif -%}

  <table>
    <thead>
      <tr>
        <th scope="col">{{ section.settings.qty_label }}</th>
        <th scope="col">{{ section.settings.price_label }}</th>
        <th scope="col">{{ section.settings.save_label }}</th>
      </tr>
    </thead>
    <tbody>
      {%- for block in section.blocks -%}
        {%- liquid
          assign keep = 100 | minus: block.settings.discount
          assign unit = base_price | times: keep | divided_by: 100
        -%}
        <tr data-min="{{ block.settings.min_qty }}" data-discount="{{ block.settings.discount }}" {{ block.shopify_attributes }}>
          <td>
            {{ block.settings.min_qty }}+
            {%- if block.settings.badge != blank -%}<span class="lt-qpb__badge">{{ block.settings.badge }}</span>{%- endif -%}
          </td>
          <td data-unit>{{ unit | money }}</td>
          <td class="lt-qpb__save">
            {%- if block.settings.discount > 0 -%}{{ block.settings.discount }}%{%- else -%}&mdash;{%- endif -%}
          </td>
        </tr>
      {%- endfor -%}
    </tbody>
  </table>

  {%- if section.settings.note != blank -%}
    <p class="lt-qpb__note">{{ section.settings.note }}</p>
  {%- endif -%}

  {%- if product != blank -%}
    <script type="application/json" data-variants>
      { {%- for v in product.variants -%}"{{ v.id }}": {{ v.price }}{%- unless forloop.last -%},{%- endunless -%}{%- endfor -%} }
    </script>
  {%- endif -%}
</div>

<script>
  (function () {
    var root = document.getElementById('lt-{{ section.id }}');
    if (!root || root.dataset.ready) return;
    root.dataset.ready = '1';

    var rows = Array.prototype.slice.call(root.querySelectorAll('tbody tr'));
    var variantsEl = root.querySelector('[data-variants]');
    var prices = variantsEl ? JSON.parse(variantsEl.textContent) : {};
    var form = document.querySelector('main form[action*="/cart/add"]') || document.querySelector('form[action*="/cart/add"]');

    function quantityInput() {
      return document.querySelector('input[name="quantity"]');
    }

    function money(cents) {
      var currency = (window.Shopify && Shopify.currency && Shopify.currency.active) || root.dataset.currency;
      return new Intl.NumberFormat(document.documentElement.lang || undefined, { style: 'currency', currency: currency }).format(cents / 100);
    }

    function highlight() {
      var input = quantityInput();
      var qty = input ? Number(input.value) || 1 : 1;
      var active = null;
      rows.forEach(function (row) { if (qty >= Number(row.dataset.min)) active = row; });
      rows.forEach(function (row) { row.classList.toggle('is-active', row === active); });
    }

    function reprice() {
      var idInput = form && form.querySelector('[name="id"]');
      var price = idInput ? prices[idInput.value] : null;
      if (price == null) return;
      rows.forEach(function (row) {
        var unit = Math.floor(price * (100 - Number(row.dataset.discount)) / 100);
        row.querySelector('[data-unit]').textContent = money(unit);
      });
    }

    rows.forEach(function (row) {
      row.addEventListener('click', function () {
        var input = quantityInput();
        if (!input) return;
        input.value = row.dataset.min;
        input.dispatchEvent(new Event('change', { bubbles: true }));
        highlight();
      });
    });

    document.addEventListener('change', function () { highlight(); reprice(); });
    document.addEventListener('input', function (event) {
      if (event.target && event.target.name === 'quantity') highlight();
    });
    document.addEventListener('click', function () { setTimeout(highlight, 0); });
    highlight();
  })();
</script>

{% schema %}
{
  "name": "Quantity price breaks",
  "tag": "section",
  "class": "lt-qpb-section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Buy more, save more"
    },
    {
      "type": "text",
      "id": "qty_label",
      "label": "Quantity column",
      "default": "Quantity"
    },
    {
      "type": "text",
      "id": "price_label",
      "label": "Price column",
      "default": "Price each"
    },
    {
      "type": "text",
      "id": "save_label",
      "label": "Saving column",
      "default": "You save"
    },
    {
      "type": "text",
      "id": "note",
      "label": "Note under the table",
      "default": "Discount applied automatically at checkout."
    },
    {
      "type": "color",
      "id": "border_color",
      "label": "Border colour",
      "default": "#e5e5e5"
    },
    {
      "type": "color",
      "id": "highlight",
      "label": "Active row colour",
      "default": "#f0fdf4"
    },
    {
      "type": "color",
      "id": "save_color",
      "label": "Saving colour",
      "default": "#16a34a"
    },
    {
      "type": "color",
      "id": "badge_color",
      "label": "Badge colour",
      "default": "#111111"
    },
    {
      "type": "color",
      "id": "badge_text",
      "label": "Badge text colour",
      "default": "#ffffff"
    },
    {
      "type": "range",
      "id": "padding",
      "label": "Vertical padding",
      "min": 0,
      "max": 60,
      "step": 4,
      "unit": "px",
      "default": 16
    }
  ],
  "blocks": [
    {
      "type": "tier",
      "name": "Tier",
      "settings": [
        {
          "type": "number",
          "id": "min_qty",
          "label": "Minimum quantity",
          "default": 2
        },
        {
          "type": "range",
          "id": "discount",
          "label": "Discount",
          "min": 0,
          "max": 80,
          "step": 1,
          "unit": "%",
          "default": 10
        },
        {
          "type": "text",
          "id": "badge",
          "label": "Badge",
          "info": "Optional, for example Most popular."
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Quantity price breaks",
      "blocks": [
        { "type": "tier", "settings": { "min_qty": 1, "discount": 0 } },
        { "type": "tier", "settings": { "min_qty": 2, "discount": 10 } },
        { "type": "tier", "settings": { "min_qty": 3, "discount": 15, "badge": "Most popular" } },
        { "type": "tier", "settings": { "min_qty": 5, "discount": 20 } }
      ]
    }
  ]
}
{% endschema %}
Customization

What you can change in the theme editor

Setting Default
Heading Buy more, save more
Quantity column Quantity
Price column Price each
Saving column You save
Note under the table Discount applied automatically at checkout.
Border colour #e5e5e5
Active row colour #f0fdf4
Saving colour #16a34a
Badge colour #111111
Badge text colour #ffffff
Vertical padding 16px

Blocks

Add, remove and reorder these inside the section:

  • Tier : minimum quantity, discount, and badge
Good to know

Tips for setting it up

  • This table only displays the prices. Create a matching automatic discount in Shopify admin > Discounts (Amount off products, with a minimum quantity) so checkout charges the same amount.
  • Add a tier with a minimum quantity of 1 and 0% discount so shoppers see the starting price.
  • Use the badge on one tier, such as Most popular, to steer shoppers to the quantity you want them to buy.

Further reading: Upsell revenue calculator

FAQ

Frequently asked questions

Will the discount be applied at checkout?

Only if you create the discount in Shopify. The table shows the maths; Shopify Discounts does the charging.

Does it update when the variant changes?

Yes. It re-prices each tier when the shopper picks a variant with a different price.

Can I use fixed prices instead of percentages?

The table uses percentages so it works for every variant. For fixed tier prices, edit the unit calculation in the Liquid to subtract a fixed amount.

SellUp

Want this without the code?

Quantity tiers grow orders of one product. SellUp grows the rest of the basket with cross-sells and add-ons on the product page, in the cart and after checkout.

Try SellUp

More free sections

New

Gift Wrap Checkbox

A paid gift wrap add-on with an optional gift message, right above add to cart.

New

Frequently Bought Together

Hand-picked product bundles with checkboxes, a running total and one add-all button.

New

Free Shipping Progress Bar

"You're £12 away from free shipping" with a bar that fills as the cart grows.

Explore More Free Sections

Discover more code snippets to enhance your Shopify store.

View All Sections
SellUp Increase AOV with Upsells