Frequently Bought Together

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

One section file Product page, below the product information Edit in the theme editor

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

A frequently bought together block puts the obvious next purchase in front of the shopper while they're still deciding. This version lets you hand-pick up to four products, shows a live total, and adds everything that's ticked to the cart in one click.

Preview

What you'll build

Frequently bought together

This item Trail Running Shoe £89.00
Merino Running Socks £14.00
Waterproof Shoe Spray £9.50
Total: £112.50 Add selected to cart

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-frequently-bought-together, 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 Bought together.

  4. 4

    Make it yours

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

sections/lt-frequently-bought-together.liquid · 273 lines
{%- comment -%}
  Frequently bought together (hand-picked products) by LAUNCHTIP
  https://launchtip.com/free-shopify-sections-code
{%- endcomment -%}
{%- liquid
  assign picks = section.settings.products
  assign show_current = false
  if section.settings.include_current and product != blank
    assign show_current = true
  endif
  assign total = 0
  assign count = 0
-%}

{%- style -%}
  #lt-{{ section.id }} { padding: {{ section.settings.padding }}px 0; }
  #lt-{{ section.id }} .lt-fbt { border: 1px solid {{ section.settings.border_color }}; border-radius: 10px; padding: 20px; }
  #lt-{{ section.id }} .lt-fbt__heading { margin: 0 0 14px; }
  #lt-{{ section.id }} .lt-fbt__item { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-top: 1px solid {{ section.settings.border_color }}; }
  #lt-{{ section.id }} .lt-fbt__item:first-of-type { border-top: 0; }
  #lt-{{ section.id }} .lt-fbt__item input { width: 18px; height: 18px; accent-color: {{ section.settings.button_color }}; flex-shrink: 0; }
  #lt-{{ section.id }} .lt-fbt__item img { width: 56px; height: 56px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
  #lt-{{ section.id }} .lt-fbt__name { flex: 1; min-width: 0; }
  #lt-{{ section.id }} .lt-fbt__name a { color: inherit; text-decoration: none; }
  #lt-{{ section.id }} .lt-fbt__tag { display: block; font-size: 0.8em; opacity: 0.6; }
  #lt-{{ section.id }} .lt-fbt__price { font-weight: 600; white-space: nowrap; }
  #lt-{{ section.id }} .lt-fbt__footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; }
  #lt-{{ section.id }} .lt-fbt__total { font-size: 1.1em; }
  #lt-{{ section.id }} .lt-fbt__button { background: {{ section.settings.button_color }}; color: {{ section.settings.button_text }}; border: 0; border-radius: 6px; padding: 12px 20px; font: inherit; font-weight: 600; cursor: pointer; }
  #lt-{{ section.id }} .lt-fbt__button[disabled] { opacity: 0.5; cursor: default; }
{%- endstyle -%}

<div
  id="lt-{{ section.id }}"
  class="page-width"
  data-add-url="{{ routes.cart_add_url }}.js"
  data-cart-url="{{ routes.cart_url }}"
  data-currency="{{ cart.currency.iso_code }}"
  data-after-add="{{ section.settings.after_add }}"
>
  <div class="lt-fbt">
    {%- if section.settings.heading != blank -%}
      <h2 class="lt-fbt__heading h3">{{ section.settings.heading }}</h2>
    {%- endif -%}

    {%- for i in (0..4) -%}
      {%- liquid
        if i == 0
          unless show_current
            continue
          endunless
          assign p = product
        else
          assign idx = i | minus: 1
          assign p = picks[idx]
          if p == blank
            continue
          endif
          if show_current and p.id == product.id
            continue
          endif
        endif
        assign v = p.selected_or_first_available_variant
        unless v.available
          continue
        endunless
        assign total = total | plus: v.price
        assign count = count | plus: 1
      -%}
      <label class="lt-fbt__item" {% if i == 0 %}data-current{% endif %}>
        <input type="checkbox" checked data-id="{{ v.id }}" data-price="{{ v.price }}">
        {%- if p.featured_image != blank -%}
          {{ p.featured_image | image_url: width: 160 | image_tag: loading: 'lazy', alt: p.title }}
        {%- endif -%}
        <span class="lt-fbt__name">
          {%- if i == 0 -%}
            <span class="lt-fbt__tag">{{ section.settings.current_label }}</span>
            {{ p.title }}
          {%- else -%}
            <a href="{{ p.url }}">{{ p.title }}</a>
          {%- endif -%}
        </span>
        <span class="lt-fbt__price">{{ v.price | money }}</span>
      </label>
    {%- endfor -%}

    {%- if count == 0 and request.design_mode -%}
      <p>Pick up to 4 products in the section settings.</p>
    {%- endif -%}

    {%- if count > 0 -%}
      <div class="lt-fbt__footer">
        <span class="lt-fbt__total">{{ section.settings.total_label }} <strong data-total>{{ total | money }}</strong></span>
        <button type="button" class="lt-fbt__button" data-label="{{ section.settings.button_label | escape }}">{{ section.settings.button_label }}</button>
      </div>
    {%- endif -%}
  </div>

  {%- if show_current -%}
    <script type="application/json" data-variants>
      { {%- for v in product.variants -%}"{{ v.id }}": { "price": {{ v.price }}, "available": {{ v.available }} }{%- 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 button = root.querySelector('.lt-fbt__button');
    var totalEl = root.querySelector('[data-total]');
    if (!button) return;

    var variantsEl = root.querySelector('[data-variants]');
    var variants = variantsEl ? JSON.parse(variantsEl.textContent) : {};

    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 checked() {
      return Array.prototype.filter.call(root.querySelectorAll('input[data-id]'), function (input) { return input.checked; });
    }

    function update() {
      var picked = checked();
      var total = picked.reduce(function (sum, input) { return sum + Number(input.dataset.price); }, 0);
      totalEl.textContent = money(total);
      button.disabled = picked.length === 0;
    }

    root.addEventListener('change', update);

    var current = root.querySelector('[data-current] input');
    var form = document.querySelector('main form[action*="/cart/add"]') || document.querySelector('form[action*="/cart/add"]');
    if (current && form) {
      form.addEventListener('change', function () {
        var idInput = form.querySelector('[name="id"]');
        var variant = idInput && variants[idInput.value];
        if (!variant) return;
        current.dataset.id = idInput.value;
        current.dataset.price = variant.price;
        current.disabled = !variant.available;
        if (!variant.available) current.checked = false;
        update();
      });
    }

    button.addEventListener('click', function () {
      var items = checked().map(function (input) { return { id: Number(input.dataset.id), quantity: 1 }; });
      if (!items.length) return;
      button.disabled = true;
      fetch(root.dataset.addUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
        body: JSON.stringify({ items: items })
      })
        .then(function (response) {
          if (!response.ok) throw new Error('add failed');
          if (root.dataset.afterAdd === 'cart') {
            window.location.href = root.dataset.cartUrl;
            return;
          }
          button.textContent = {{ section.settings.added_label | json }};
          document.dispatchEvent(new CustomEvent('cart:refresh', { bubbles: true }));
          setTimeout(function () { button.textContent = button.dataset.label; button.disabled = false; }, 2500);
        })
        .catch(function () {
          button.textContent = button.dataset.label;
          button.disabled = false;
        });
    });
  })();
</script>

{% schema %}
{
  "name": "Bought together",
  "tag": "section",
  "class": "lt-fbt-section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Frequently bought together"
    },
    {
      "type": "product_list",
      "id": "products",
      "label": "Products",
      "limit": 4,
      "info": "Hand-pick up to 4 products that go with this one."
    },
    {
      "type": "checkbox",
      "id": "include_current",
      "label": "Include the product being viewed",
      "default": true
    },
    {
      "type": "text",
      "id": "current_label",
      "label": "Current product label",
      "default": "This item"
    },
    {
      "type": "text",
      "id": "total_label",
      "label": "Total label",
      "default": "Total:"
    },
    {
      "type": "text",
      "id": "button_label",
      "label": "Button label",
      "default": "Add selected to cart"
    },
    {
      "type": "text",
      "id": "added_label",
      "label": "Added label",
      "default": "Added to cart"
    },
    {
      "type": "select",
      "id": "after_add",
      "label": "After adding",
      "options": [
        { "value": "cart", "label": "Go to the cart page" },
        { "value": "stay", "label": "Stay on the page" }
      ],
      "default": "cart"
    },
    {
      "type": "color",
      "id": "button_color",
      "label": "Button colour",
      "default": "#111111"
    },
    {
      "type": "color",
      "id": "button_text",
      "label": "Button text colour",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "border_color",
      "label": "Border colour",
      "default": "#e5e5e5"
    },
    {
      "type": "range",
      "id": "padding",
      "label": "Vertical padding",
      "min": 0,
      "max": 80,
      "step": 4,
      "unit": "px",
      "default": 24
    }
  ],
  "presets": [
    {
      "name": "Bought together"
    }
  ]
}
{% endschema %}
Customization

What you can change in the theme editor

Setting Default
Heading Frequently bought together
Products Hand-pick up to 4 products that go with this one. —
Include the product being viewed On
Current product label This item
Total label Total:
Button label Add selected to cart
Added label Added to cart
After adding Go to the cart page
Button colour #111111
Button text colour #ffffff
Border colour #e5e5e5
Vertical padding 24px
Good to know

Tips for setting it up

  • Create a separate product template for each product family (for example product.cameras) so each family can have its own hand-picked items.
  • Products with several variants are added as their first available variant. Pick single-variant accessories for the best results.
  • Set After adding to Go to the cart page if your theme's cart drawer doesn't refresh on its own.

Further reading: How to upsell on Shopify

FAQ

Frequently asked questions

Can I show different products on each product page?

The section's products are set per template. Create one product template per group of products, add the section to each, and assign products to the right template.

Can I give a discount for buying them together?

This section adds the items at their normal price. Create an automatic discount in Shopify admin to reward the bundle, or use an app that discounts bundles for you.

What happens if a picked product sells out?

Sold-out products are skipped automatically, so the block never offers something the shopper can't buy.

SellUp

Want this without the code?

Hand-picking works for a few hero products. SellUp uses AI to match frequently bought together offers and add-ons to every product in your catalogue, so you don't have to set them up template by template.

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

Free Shipping Progress Bar

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

New

Quantity Price-Break Table

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

Explore More Free Sections

Discover more code snippets to enhance your Shopify store.

View All Sections
SellUp Increase AOV with Upsells