{%- comment -%} MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections {%- endcomment -%} {%- comment -%} Product bundle ("buy it together"): two or three products you pick, each with its image, price and variant menu, a running total and one button that adds them all to the cart through Shopify's Cart AJAX API (/cart/add.js). Shoppers can leave a product out with its checkbox. The section only displays prices: a bundle discount has to be set up in Shopify (Discounts), and the optional discount note is text you type. Without JavaScript the form posts every available product to /cart/add. Sold-out products are shown but never added. Depends on: nothing. {%- endcomment -%} {%- liquid assign s = section.settings assign heading_tag = s.heading_tag | default: 'h2' assign uid = 'LibBundle-' | append: section.id -%}
{%- if s.heading != blank or s.text != blank -%}
{%- if s.heading != blank -%} <{{ heading_tag }} class="lib-bundle__heading lib-bundle__heading--{{ s.heading_size }}" id="{{ uid }}-title"> {{- s.heading -}} {%- endif -%} {%- if s.text != blank -%}
{{ s.text }}
{%- endif -%}
{%- endif -%}
    {%- for block in section.blocks -%} {%- liquid assign b = block.settings assign p = b.product if p == blank and request.design_mode == false continue endif assign item_id = uid | append: '-' | append: block.id assign v = p.selected_or_first_available_variant assign qty = b.quantity | default: 1 -%}
  • {%- if p.featured_image != blank -%} {{ p.featured_image | image_url: width: 600 | image_tag: class: 'lib-bundle__img', loading: 'lazy', alt: p.featured_image.alt, widths: '200, 400, 600', sizes: '(min-width: 990px) 20vw, 30vw' }} {%- else -%} {{ 'product-1' | placeholder_svg_tag: 'lib-bundle__img lib-bundle__img--ph' }} {%- endif -%}
    {%- if p != blank and p.available -%}

    {%- if qty > 1 %}{{ qty }} × {% endif %}{{ p.title | escape -}}

    {%- else -%}

    {{ p.title | default: 'Example product' | escape }}

    {%- endif -%} {%- if qty > 1 and p.available -%} {%- endif -%}

    {%- if p == blank -%} {{ 4800 | money }} {%- elsif p.available == false -%} {{ s.sold_out_label | escape }} {%- else -%} {%- assign on_sale = false -%} {%- if v.compare_at_price > v.price -%}{%- assign on_sale = true -%}{%- endif -%} {{ s.sale_label | escape }} {{ v.price | money }} {{ s.regular_label | escape }} {% if on_sale %}{{ v.compare_at_price | money }}{% endif %} {%- endif -%}

    {%- if p != blank and p.available -%} {%- if p.has_only_default_variant -%} {%- else -%} {%- endif -%} {%- endif -%}
  • {%- endfor -%}
{%- if s.discount_note != blank -%}

{{ s.discount_note | escape }}

{%- endif -%}

{% stylesheet %} .lib-bundle { background-color: var(--lib-bg); background-image: var(--lib-bg-image, none); color: var(--lib-text); padding: var(--lib-pt) 1rem var(--lib-pb); } .lib-bundle__inner { max-width: 1200px; margin: 0 auto; display: grid; gap: clamp(1.5rem, 3vw, 2.5rem); } .lib-bundle--full .lib-bundle__inner { max-width: none; } @media (max-width: 749px) { .lib-bundle { padding-top: var(--lib-pt-m); padding-bottom: var(--lib-pb-m); } .lib-bundle[data-show='desktop'] { display: none; } } @media (min-width: 750px) { .lib-bundle[data-show='mobile'] { display: none; } } .lib-bundle__header { text-align: left; } .lib-bundle__header[data-align='center'] { text-align: center; } .lib-bundle__header[data-align='right'] { text-align: right; } @media (max-width: 749px) { .lib-bundle__header[data-align-m='left'] { text-align: left; } .lib-bundle__header[data-align-m='center'] { text-align: center; } .lib-bundle__header[data-align-m='right'] { text-align: right; } } .lib-bundle__heading { margin: 0; font-size: clamp(1.75rem, 3.2vw, 2.5rem); line-height: 1.1; letter-spacing: -0.02em; text-wrap: balance; } .lib-bundle__heading--small { font-size: clamp(1.4rem, 2.2vw, 1.75rem); } .lib-bundle__heading--large { font-size: clamp(2rem, 4.2vw, 3.25rem); } .lib-bundle__text { max-width: 40rem; margin-top: 0.75rem; font-size: 1.0625rem; line-height: 1.6; opacity: 0.8; } .lib-bundle__header[data-align='center'] .lib-bundle__text { margin-inline: auto; } .lib-bundle__header[data-align='right'] .lib-bundle__text { margin-inline-start: auto; } .lib-bundle__text > :first-child { margin-top: 0; } .lib-bundle__text > :last-child { margin-bottom: 0; } .lib-bundle__form { display: grid; gap: 1.5rem; } @media (min-width: 990px) { .lib-bundle__form { grid-template-columns: minmax(0, 1fr) 18rem; align-items: center; gap: 2.5rem; } } .lib-bundle__items { display: grid; gap: 0.75rem; margin: 0; padding: 0; list-style: none; } @media (min-width: 750px) { .lib-bundle__items { grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 2.75rem; } /* The plus sign between products. */ .lib-bundle__item + .lib-bundle__item::before { content: '+'; position: absolute; left: -1.375rem; top: 35%; translate: -50% -50%; font-size: 1.5rem; font-weight: 300; opacity: 0.55; } } .lib-bundle__item { position: relative; display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: 1rem; align-items: center; padding: 0.75rem; border-radius: 14px; background: var(--lib-card); transition: opacity 0.2s ease; } @media (min-width: 750px) { .lib-bundle__item { grid-template-columns: 1fr; align-items: start; align-content: start; padding: 0; background: none; } } .lib-bundle__item.is-out, .lib-bundle__item.is-sold-out { opacity: 0.5; } .lib-bundle__media { overflow: hidden; border-radius: 10px; aspect-ratio: 1; background: color-mix(in srgb, var(--lib-text) 6%, transparent); } .lib-bundle[data-ratio='portrait'] .lib-bundle__media { aspect-ratio: 4 / 5; } .lib-bundle__img { display: block; width: 100%; height: 100%; object-fit: cover; } .lib-bundle__img--ph { fill: color-mix(in srgb, var(--lib-text) 22%, transparent); } .lib-bundle__body { display: grid; gap: 0.35rem; min-width: 0; } .lib-bundle__pick { display: flex; align-items: flex-start; gap: 0.55rem; cursor: pointer; } .lib-bundle__pick[hidden], .lib-bundle__qty[hidden] { display: none; } .lib-bundle__check { flex: none; width: 1.25rem; height: 1.25rem; margin: 0.1rem 0 0; accent-color: var(--lib-accent); cursor: pointer; } .lib-bundle__check:focus-visible { outline: 2px solid var(--lib-text); outline-offset: 2px; } .lib-bundle__title { margin: 0; font-weight: 600; line-height: 1.35; } .lib-bundle__qty { margin: 0; font-size: 0.875rem; opacity: 0.75; } .lib-bundle__price { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.5rem; margin: 0; } .lib-bundle__now.is-sale { color: var(--lib-sale); font-weight: 600; } .lib-bundle__was { opacity: 0.6; font-size: 0.875em; } .lib-bundle__badge { font-size: 0.8125rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; } .lib-bundle__select { width: 100%; min-height: 2.5rem; margin-top: 0.25rem; padding: 0.4rem 0.6rem; border: 1px solid color-mix(in srgb, var(--lib-text) 35%, transparent); border-radius: 8px; background: var(--lib-bg); color: inherit; font: inherit; font-size: 0.9375rem; } .lib-bundle__select:focus-visible { outline: 2px solid var(--lib-text); outline-offset: 1px; } .lib-bundle__summary { display: grid; gap: 0.85rem; padding: 1.5rem; border-radius: 16px; background: var(--lib-card); } .lib-bundle__note { margin: 0; font-size: 0.9375rem; font-weight: 600; color: var(--lib-sale); } .lib-bundle__total { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; margin: 0; font-size: 1.0625rem; } .lib-bundle__total[hidden] { display: none; } .lib-bundle__total strong { font-size: 1.375rem; font-variant-numeric: tabular-nums; } .lib-bundle__button { display: inline-flex; justify-content: center; align-items: center; min-height: 3rem; padding: 0.75rem 1.5rem; border: 0; border-radius: 999px; background: var(--lib-accent); color: var(--lib-accent-text); font: inherit; font-weight: 600; cursor: pointer; transition: opacity 0.2s ease, transform 0.2s ease; } .lib-bundle__button:hover { opacity: 0.9; } .lib-bundle__button:focus-visible { outline: 2px solid var(--lib-text); outline-offset: 3px; } .lib-bundle__button[aria-disabled='true'] { opacity: 0.5; cursor: not-allowed; } .lib-bundle__status { margin: 0; font-size: 0.875rem; } .lib-bundle__status:empty { display: none; } .lib-bundle__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } .lib-bundle__sr[hidden] { display: none; } @media (prefers-reduced-motion: no-preference) { .lib-bundle[data-animate] .lib-bundle__item { animation: lib-bundle-in 0.6s ease-out both; } .lib-bundle[data-animate] .lib-bundle__item:nth-child(2) { animation-delay: 0.1s; } .lib-bundle[data-animate] .lib-bundle__item:nth-child(3) { animation-delay: 0.2s; } } @keyframes lib-bundle-in { from { opacity: 0; transform: translateY(10px); } } {% endstylesheet %} {% javascript %} if (!customElements.get('lib-product-bundle')) { customElements.define( 'lib-product-bundle', class extends HTMLElement { connectedCallback() { if (this.form) return; this.form = this.querySelector('form'); this.items = [...this.querySelectorAll('[data-item]')]; this.status = this.querySelector('[data-status]'); this.button = this.querySelector('[data-submit]'); this.totalEl = this.querySelector('[data-total]'); if (!this.form || !this.items.length) return; // With JavaScript, products can be left out: show the checkboxes, hide the plain titles. this.items.forEach((item) => { item.querySelector('[data-pick]')?.removeAttribute('hidden'); item.querySelector('[data-qty-label]')?.removeAttribute('hidden'); item.querySelector('[data-nojs-title]')?.remove(); item.querySelector('[data-include]')?.addEventListener('change', () => this.update()); const select = item.querySelector('select[data-variant]'); if (select) select.addEventListener('change', () => this.showVariant(item, select)); }); this.querySelector('[data-total-wrap]')?.removeAttribute('hidden'); this.form.addEventListener('submit', (event) => this.submit(event)); this.update(); } chosen() { return this.items.filter((item) => item.querySelector('[data-include]')?.checked); } cents(item) { const input = item.querySelector('[data-variant]'); const source = input?.tagName === 'SELECT' ? input.selectedOptions[0] : input; return Number(source?.dataset.cents || 0) * Number(item.dataset.qty || 1); } // Formats cents like the store does. The section renders 123456 cents with Shopify's money // filter (data-money-sample, e.g. "$1,234.56" or "1.234,56 €"); the separators, decimals and // currency sign are read from that sample, so any money format works. money(cents) { const sample = this.dataset.moneySample || ''; const match = sample.match(/1(\D?)234(\D)56|1(\D?)235/); if (!match) return (cents / 100).toFixed(2); const point = match[2] || ''; const thousands = (point ? match[1] : match[3]) || ''; const [whole, frac] = (cents / 100).toFixed(point ? 2 : 0).split('.'); const number = whole.replace(/\B(?=(\d{3})+(?!\d))/g, thousands) + (frac ? point + frac : ''); return sample.replace(match[0], number); } showVariant(item, select) { const option = select.selectedOptions[0]; const price = item.querySelector('[data-price]'); const was = item.querySelector('[data-was]'); const onSale = Boolean(option.dataset.compare); if (price) { price.textContent = option.dataset.price; price.classList.toggle('is-sale', onSale); } if (was) was.textContent = option.dataset.compare || ''; item.querySelector('[data-sale-sr]')?.toggleAttribute('hidden', !onSale); item.querySelector('[data-was-sr]')?.toggleAttribute('hidden', !onSale); this.update(); } update() { const chosen = this.chosen(); this.items.forEach((item) => item.classList.toggle('is-out', !chosen.includes(item))); if (this.totalEl) this.totalEl.textContent = this.money(chosen.reduce((sum, item) => sum + this.cents(item), 0)); // aria-disabled keeps the button focusable, so the "choose a product" message can be heard. this.button?.setAttribute('aria-disabled', String(chosen.length === 0)); } say(message) { if (!this.status) return; this.status.textContent = ''; requestAnimationFrame(() => { this.status.textContent = message; }); } async submit(event) { event.preventDefault(); const chosen = this.chosen(); if (!chosen.length) { this.say(this.dataset.msgEmpty); return; } const items = chosen.map((item) => ({ id: Number(item.querySelector('[data-variant]').value), quantity: Number(item.dataset.qty || 1), })); this.button.setAttribute('aria-busy', 'true'); try { const response = await fetch(this.dataset.addUrl, { method: 'POST', headers: {'Content-Type': 'application/json', Accept: 'application/json'}, body: JSON.stringify({items}), }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new Error(body.description || ''); document.dispatchEvent(new CustomEvent('cart:refresh', {bubbles: true, detail: {items: body.items || items}})); if (this.dataset.afterAdd === 'cart') { window.location.href = this.dataset.cartUrl; return; } this.say(this.dataset.msgAdded); } catch (error) { // Shopify's own message (e.g. a quantity limit) is more useful than the generic one. this.say(error.message || this.dataset.msgError); } finally { this.button.removeAttribute('aria-busy'); } } } ); } {% endjavascript %} {% schema %} { "name": "Lib · Product bundle", "tag": "section", "class": "lib-section", "max_blocks": 3, "settings": [ { "type": "paragraph", "content": "The section adds the products at their normal prices. For a bundle discount, create an automatic discount in Shopify (Discounts) for these products; the note below only describes it." }, { "type": "header", "content": "Content" }, { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Better together" }, { "type": "select", "id": "heading_tag", "label": "Heading tag", "info": "Use H1 only when this is the page's main heading.", "options": [ { "value": "h1", "label": "H1" }, { "value": "h2", "label": "H2" }, { "value": "h3", "label": "H3" }, { "value": "p", "label": "Paragraph" } ], "default": "h2" }, { "type": "select", "id": "heading_size", "label": "Heading size", "options": [ { "value": "small", "label": "Small" }, { "value": "medium", "label": "Medium" }, { "value": "large", "label": "Large" } ], "default": "medium" }, { "type": "richtext", "id": "text", "label": "Text", "default": "

Pieces that are made to be used together.

" }, { "type": "text", "id": "discount_note", "label": "Discount note", "info": "Optional, e.g. 10% off when bought together. Only text: set up the discount itself in Shopify." }, { "type": "header", "content": "Words" }, { "type": "text", "id": "button_label", "label": "Button", "default": "Add all to cart" }, { "type": "text", "id": "total_label", "label": "Total", "default": "Total" }, { "type": "text", "id": "include_label", "label": "Screen reader: before each product's checkbox", "default": "Include" }, { "type": "text", "id": "variant_label", "label": "Screen reader: variant menu", "default": "Options" }, { "type": "text", "id": "sold_out_label", "label": "Sold out", "default": "Sold out" }, { "type": "text", "id": "sale_label", "label": "Screen reader: sale price", "default": "Sale price" }, { "type": "text", "id": "regular_label", "label": "Screen reader: regular price", "default": "Regular price" }, { "type": "text", "id": "added_message", "label": "Message when added", "default": "Added to your cart." }, { "type": "text", "id": "error_message", "label": "Message when adding fails", "default": "These products could not be added. Please try again." }, { "type": "text", "id": "empty_message", "label": "Message when nothing is chosen", "default": "Choose at least one product." }, { "type": "select", "id": "after_add", "label": "After adding", "info": "Staying on the page sends a cart:refresh event that many themes use to update the cart count.", "options": [ { "value": "cart", "label": "Go to the cart" }, { "value": "stay", "label": "Stay and show the message" } ], "default": "cart" }, { "type": "header", "content": "Layout" }, { "type": "select", "id": "image_ratio", "label": "Image shape", "options": [ { "value": "square", "label": "Square" }, { "value": "portrait", "label": "Portrait (4:5)" } ], "default": "square" }, { "type": "select", "id": "section_width", "label": "Width", "options": [ { "value": "page", "label": "Page width" }, { "value": "full", "label": "Full width" } ], "default": "page" }, { "type": "text_alignment", "id": "text_alignment", "label": "Heading alignment on desktop", "default": "left" }, { "type": "select", "id": "text_alignment_mobile", "label": "Heading alignment on mobile", "options": [ { "value": "left", "label": "Left" }, { "value": "center", "label": "Center" }, { "value": "right", "label": "Right" } ], "default": "left" }, { "type": "header", "content": "Colors" }, { "type": "color", "id": "color_bg", "label": "Background", "default": "#FFFDF9" }, { "type": "color_background", "id": "color_bg_gradient", "label": "Background gradient", "info": "Optional. Covers the background color." }, { "type": "color", "id": "color_text", "label": "Text", "default": "#1C1917" }, { "type": "color", "id": "color_accent", "label": "Button and checkboxes", "default": "#1C1917" }, { "type": "color", "id": "color_accent_text", "label": "Button text", "default": "#FFFDF9" }, { "type": "color", "id": "color_card", "label": "Card and total background", "default": "#F5F2EC" }, { "type": "color", "id": "color_sale", "label": "Sale price and discount note", "default": "#B4432F" }, { "type": "header", "content": "Spacing and visibility" }, { "type": "range", "id": "padding_top", "label": "Top padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 72 }, { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 72 }, { "type": "range", "id": "padding_top_mobile", "label": "Top padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 44 }, { "type": "range", "id": "padding_bottom_mobile", "label": "Bottom padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 44 }, { "type": "select", "id": "show_on", "label": "Show on", "options": [ { "value": "all", "label": "All devices" }, { "value": "desktop", "label": "Desktop only" }, { "value": "mobile", "label": "Mobile only" } ], "default": "all" }, { "type": "text", "id": "anchor_id", "label": "Anchor ID", "info": "Optional. Link to this section with #your-id." }, { "type": "header", "content": "Motion" }, { "type": "checkbox", "id": "animate", "label": "Fade in the products on load", "info": "Always off for visitors who prefer reduced motion.", "default": false } ], "blocks": [ { "type": "product", "name": "Product", "limit": 3, "settings": [ { "type": "product", "id": "product", "label": "Product" }, { "type": "range", "id": "quantity", "label": "Quantity", "min": 1, "max": 5, "step": 1, "default": 1 }, { "type": "checkbox", "id": "checked", "label": "Included by default", "default": true } ] } ], "presets": [ { "name": "Lib · Product bundle", "blocks": [ { "type": "product" }, { "type": "product" }, { "type": "product" } ] } ] } {% endschema %}