Need Help With Your Shopify Store?
I'm a Shopify expert developer working as a solo specialist. When you reach out, you work directly with me, no project managers or middlemen.
If you need help with Shopify development, improving conversions, or increasing your store’s average order value, feel free to message me on WhatsApp for a quick response.
We can work through Upwork or you can pay securely via Payoneer.
Message me on WhatsAppShopify Bundle Builder without APP
We will create Bundle Builder in Shopify without APP using metafield and metaobject with a few lines of code.
Steps
- Create the
bundle_offerMetaobject Type. Go to: Settings → Custom Data → Metaobjects → Add definition
Fields
| Field Label | Handle | Type | Notes |
|---|---|---|---|
| Title | title | Single line text | e.g. “Buy 3, Save 30%“ |
| Original Price | original_price | Decimal number | e.g. 21.00 |
| Final Price | final_price | Decimal number | e.g. 14.70 |
| Quantity | quantity | Integer | Number of slots: 3, 5, 7 |
| Collection | collection | Collection reference | Products users pick from |
| Badge Text | badge_text | Single line text | e.g. “Save 30%“ |
| Badge Color | badge_color | Color | Hex color for badge background |
- Create a file in assets folder
bundle-builder.cssand paste following content there:
/* ============================================================
Bundle Builder — bundle-builder.css
============================================================ */
/* ── Variables ───────────────────────────────────────────── */
/* ── Updated Variables to Match Image ────────────────────── */
:root {
--bb-cream: #ffffff; /* Changed to white for a cleaner look */
--bb-cream-dark: #f4f4f4;
--bb-yellow: #1a1a1a; /* Main button color (Dark) */
--bb-yellow-hover: #333333; /* Slightly lighter on hover */
--bb-purple: #1a1a1a; /* Active tabs/badges (Dark) */
--bb-purple-mid: #1a1a1a; /* Borders and accents */
--bb-purple-light: #f4f4f4; /* Disabled/Background states */
--bb-orange: #1a1a1a; /* Summary CTA color */
--bb-orange-hover: #333333;
--bb-dark: #1a1a1a;
--bb-gray: #888;
--bb-border: #e5e5e5; /* Lighter border matching the cart divider */
}
/* ── Wrapper ──────────────────────────────────────────────── */
.bundle-builder {
padding: 48px 20px 80px;
max-width: 1200px;
margin: 0 auto;
}
/* ── Header ──────────────────────────────────────────────── */
.bb-header {
text-align: center;
margin-bottom: 40px;
}
.bb-header__title {
font-size: 32px;
font-weight: 800;
margin: 0 0 14px;
line-height: 1.2;
}
.bb-header__sub {
font-size: 15px;
color: #555;
max-width: 540px;
margin: 0 auto 20px;
line-height: 1.65;
}
.bb-header__rule {
border: none;
border-top: 1px solid #e0e0e0;
max-width: 700px;
margin: 0 auto;
}
/* ── Two-Column Layout ───────────────────────────────────── */
.bb-layout {
display: grid;
grid-template-columns: 1fr 300px;
gap: 40px;
align-items: start;
margin-top: 36px;
}
@media (max-width: 900px) {
.bb-layout {
grid-template-columns: 1fr;
}
.bb-summary {
display: none;
}
.bb-picker {
padding-bottom: 150px;
}
}
/* ── Filter Tabs ─────────────────────────────────────────── */
.bb-filter-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 24px;
}
.bb-filter-tab {
padding: 7px 18px;
border: 2px solid #ddd;
border-radius: 50px;
background: #fff;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.bb-filter-tab:hover {
border-color: var(--bb-purple-mid);
color: var(--bb-purple-mid);
}
.bb-filter-tab--active {
background: var(--bb-purple);
color: #fff;
border-color: var(--bb-purple);
}
/* ── Product Grid ─────────────────────────────────────────── */
.bb-product-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
@media (max-width: 900px) {
.bb-product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* ── Product Card ─────────────────────────────────────────── */
.bb-pcard {
background: var(--bb-cream);
border-radius: 5px;
overflow: hidden;
display: flex;
flex-direction: column;
transition: box-shadow 0.2s;
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.05);
}
.bb-pcard:hover {
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
.bb-pcard__image {
aspect-ratio: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
background: var(--bb-cream);
overflow: hidden;
}
.bb-pcard__image img {
width: 100%;
height: 100%;
object-fit: contain;
transition: transform 0.3s;
}
.bb-pcard:hover .bb-pcard__image img {
transform: scale(1.05);
}
.bb-pcard__info {
padding: 8px 12px 14px;
display: flex;
flex-direction: column;
flex: 1;
}
.bb-pcard__name {
font-size: 13px;
font-weight: 700;
margin: 0 0 6px;
line-height: 1.3;
}
.bb-pcard__divider {
border: none;
border-top: 1px solid rgba(0,0,0,0.10);
margin: 0 0 8px;
}
.bb-pcard__details {
font-size: 11px;
color: var(--bb-dark);
text-decoration: underline;
margin: 0 0 10px;
background: none;
border: none;
padding: 0;
cursor: pointer;
text-align: left;
display: inline;
}
.bb-pcard__add-wrap {
margin-top: auto;
position: relative;
}
.bb-pcard__add-btn {
width: 100%;
padding: 11px 12px;
background: var(--bb-yellow);
color: #fff;
border: none;
border-radius: 50px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
letter-spacing: 0.02em;
}
.bb-pcard__add-btn:hover:not(:disabled):not(.bb-pcard__add-btn--full) {
background: var(--bb-yellow-hover);
}
.bb-pcard__add-btn--full {
background: var(--bb-cream-dark);
color: #999;
cursor: default;
}
.bb-pcard__add-btn--soldout {
background: #eee;
color: #aaa;
cursor: not-allowed;
text-decoration: line-through;
}
.bb-pcard__count {
position: absolute;
top: -9px;
right: -4px;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--bb-purple);
color: #fff;
font-size: 11px;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
/* ── Summary Panel (right desktop) ──────────────────────── */
.bb-summary {
position: sticky;
top: 24px;
background: #fff;
border: 1.5px solid var(--bb-border);
border-radius: 18px;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ── Deal Picker ─────────────────────────────────────────── */
.bb-deal-picker {
padding: 18px 18px 14px;
border-bottom: 1px solid var(--bb-border);
background-color: #dedede;
}
.bb-deal-headline {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.bb-deal-headline__title {
font-size: 22px;
font-weight: 800;
color: var(--bb-orange);
line-height: 1;
}
.bb-deal-headline__per {
font-size: 13px;
color: var(--bb-dark);
font-weight: 400;
}
.bb-deal-btns {
display: flex;
gap: 6px;
}
.bb-deal-btn-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
flex: 1;
min-width: 0;
position: relative;
}
.bb-deal-badge {
display: inline-block;
background: var(--bb-yellow);
color: var(--bb-dark);
font-size: 10px;
font-weight: 800;
letter-spacing: .04em;
padding: 2px 8px;
border-radius: 20px;
text-transform: uppercase;
white-space: nowrap;
line-height: 1.5;
position: absolute;
top: 0px;
transform: translateY(-50%);
}
.bb-deal-badge--ghost {
visibility: hidden;
}
.bb-deal-btn {
width: 100%;
padding: 10px 4px;
border: 2px solid var(--bb-border);
border-radius: 8px;
background: #fff;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
text-align: center;
color: var(--bb-dark);
letter-spacing: 0.03em;
white-space: nowrap;
}
.bb-deal-btn:hover:not(.bb-deal-btn--active) {
border-color: var(--bb-purple-mid);
color: var(--bb-purple-mid);
}
.bb-deal-btn--active {
background: var(--bb-purple);
border-color: var(--bb-purple);
color: #fff;
}
.bb-deal-shipping {
margin: 12px 0 0;
font-size: 12px;
font-weight: 700;
color: var(--bb-purple);
text-align: center;
letter-spacing: 0.01em;
}
/* ── Slots List ──────────────────────────────────────────── */
.bb-slots {
flex: 1;
max-height: 340px;
overflow-y: auto;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 7px;
scrollbar-width: thin;
scrollbar-color: var(--bb-border) transparent;
}
.bb-slots::-webkit-scrollbar { width: 4px; }
.bb-slots::-webkit-scrollbar-thumb { background: var(--bb-border); border-radius: 2px; }
.bb-slot {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 2px dashed var(--bb-purple-mid);
border-radius: 10px;
min-height: 52px;
transition: background 0.15s, border-color 0.15s;
}
.bb-slot--empty {
justify-content: center;
}
.bb-slot--filled {
border-style: solid;
border-color: #d4ccea;
background: #faf8ff;
}
.bb-slot__placeholder {
font-size: 13px;
font-weight: 600;
color: var(--bb-purple-mid);
font-style: italic;
}
.bb-slot__thumb {
width: 36px;
height: 36px;
border-radius: 6px;
object-fit: cover;
flex-shrink: 0;
background: var(--bb-cream);
}
.bb-slot__info {
flex: 1;
min-width: 0;
}
.bb-slot__name {
font-size: 12px;
font-weight: 700;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bb-slot__variant {
font-size: 10px;
color: var(--bb-gray);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bb-slot__remove {
width: 22px;
height: 22px;
border-radius: 50%;
background: #e0e0e0;
border: none;
color: #666;
font-size: 11px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background 0.15s, color 0.15s;
line-height: 1;
}
.bb-slot__remove:hover {
background: #ff3b30;
color: #fff;
}
/* ── Summary CTA ─────────────────────────────────────────── */
.bb-summary__footer {
padding: 12px 14px 16px;
border-top: 1px solid var(--bb-border);
background-color: #dedede;
}
.bb-summary__add-btn {
display: block;
width: 100%;
padding: 15px;
background: var(--bb-orange);
color: #fff;
border: none;
border-radius: 50px;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.2s, opacity 0.2s;
}
.bb-summary__add-btn:hover:not(:disabled) {
background: var(--bb-orange-hover);
}
.bb-summary__add-btn:disabled {
background: var(--bb-purple-light);
color: var(--bb-purple-mid);
cursor: not-allowed;
}
/* ── Mobile Sticky Footer ────────────────────────────────── */
.bb-mobile-footer {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-top: 1px solid #e5e5e5;
z-index: 200;
padding-bottom: max(0px, env(safe-area-inset-bottom, 0px));
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.10);
}
.bb-mobile-footer--visible {
display: block;
}
@media (min-width: 901px) {
.bb-mobile-footer--visible {
display: none !important;
}
}
.bb-mobile-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 12px 16px;
background: none;
border: none;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
font-weight: 700;
cursor: pointer;
color: var(--bb-dark);
letter-spacing: 0.01em;
}
.bb-mobile-toggle-chevron {
font-size: 16px;
transition: transform 0.25s;
color: var(--bb-dark);
}
.bb-mobile-toggle-chevron--open {
transform: rotate(180deg);
}
.bb-mobile-bundle {
max-height: 40vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
background: #fafafa;
}
.bb-mobile-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
border-bottom: 1px solid #efefef;
background: #fff;
}
.bb-mobile-item__thumb {
width: 44px;
height: 44px;
border-radius: 8px;
object-fit: cover;
background: var(--bb-cream);
flex-shrink: 0;
}
.bb-mobile-item__label {
flex: 1;
min-width: 0;
}
.bb-mobile-item__name {
font-size: 13px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bb-mobile-item__variant {
font-size: 11px;
color: var(--bb-gray);
}
.bb-mobile-stepper {
display: flex;
align-items: center;
border: 1.5px solid var(--bb-purple-mid);
border-radius: 50px;
overflow: hidden;
}
.bb-mobile-stepper__btn {
width: 32px;
height: 32px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--bb-purple-mid);
transition: background 0.15s;
line-height: 1;
}
.bb-mobile-stepper__btn:hover {
background: var(--bb-purple-light);
}
.bb-mobile-stepper__qty {
min-width: 28px;
text-align: center;
font-size: 13px;
font-weight: 800;
color: var(--bb-purple);
}
.bb-mobile-add-btn {
display: block;
width: calc(100% - 32px);
margin: 10px 16px 14px;
padding: 16px;
background: var(--bb-orange);
color: #fff;
border: none;
border-radius: 50px;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.15s;
}
.bb-mobile-add-btn:hover:not(:disabled) {
background: var(--bb-orange-hover);
}
.bb-mobile-add-btn:disabled {
background: var(--bb-purple-light);
color: var(--bb-purple-mid);
cursor: not-allowed;
}
/* ── Overlay ──────────────────────────────────────────────── */
.bb-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.50);
z-index: 900;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.bb-overlay--open {
opacity: 1;
pointer-events: all;
}
/* ── Variant / Size Drawer ────────────────────────────────── */
.bb-drawer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-radius: 20px 20px 0 0;
z-index: 901;
transform: translateY(102%);
transition: transform 0.35s cubic-bezier(0.32, 0.72, 0, 1);
max-height: 70vh;
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom, 0px);
}
.bb-drawer--open {
transform: translateY(0);
}
.bb-drawer__handle {
width: 40px;
height: 4px;
background: #ddd;
border-radius: 2px;
margin: 12px auto 0;
flex-shrink: 0;
}
.bb-drawer__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
border-bottom: 1px solid #eee;
flex-shrink: 0;
}
.bb-drawer__title {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
margin: 0;
}
.bb-drawer__close {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
color: #666;
padding: 4px;
line-height: 1;
border-radius: 4px;
transition: color 0.15s;
}
.bb-drawer__close:hover {
color: #000;
}
.bb-drawer__body {
flex: 1;
overflow-y: auto;
padding: 20px;
-webkit-overflow-scrolling: touch;
}
/* Drawer product header */
.bb-vdrawer__product {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}
.bb-vdrawer__img {
width: 64px;
height: 64px;
object-fit: cover;
border-radius: 8px;
flex-shrink: 0;
background: var(--bb-cream);
}
.bb-vdrawer__name {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
margin: 0;
line-height: 1.3;
}
.bb-vdrawer__variants {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.bb-vdrawer__btn {
padding: 11px 22px;
border: 2px solid var(--bb-border);
border-radius: 8px;
background: #fff;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.04em;
cursor: pointer;
transition: all 0.15s;
color: var(--bb-dark);
}
.bb-vdrawer__btn:hover:not(.bb-vdrawer__btn--soldout) {
border-color: var(--bb-purple);
background: var(--bb-purple);
color: #fff;
}
.bb-vdrawer__btn--soldout {
color: #ccc;
border-color: #eee;
cursor: not-allowed;
text-decoration: line-through;
}
/* Desktop: center and cap drawer */
@media (min-width: 1024px) {
.bb-drawer {
left: 50%;
right: auto;
width: 100%;
max-width: 520px;
transform: translate(-50%, 102%);
}
.bb-drawer--open {
transform: translate(-50%, 0);
}
}
/* ── Loading ──────────────────────────────────────────────── */
.bb-loading {
display: flex;
justify-content: center;
align-items: center;
padding: 60px 20px;
}
.bb-spinner {
width: 32px;
height: 32px;
border: 3px solid #eee;
border-top-color: var(--bb-purple-mid);
border-radius: 50%;
animation: bb-spin 0.7s linear infinite;
}
@keyframes bb-spin {
to { transform: rotate(360deg); }
}
.bb-error {
text-align: center;
padding: 40px 20px;
color: #999;
font-size: 14px;
}
/* ── Drawer scrollbar ────────────────────────────────────── */
.bb-drawer__body::-webkit-scrollbar { width: 4px; }
.bb-drawer__body::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; }
- Create a file in assets folder
bundle-builder.jsand paste following content there:
/* ============================================================
Bundle Builder — bundle-builder.js
============================================================ */
class BundleBuilder {
constructor() {
this.state = {
deals: [], // parsed from DOM
activeDeal: null, // { qty, collection, originalPrice, finalPrice, title, savedPct, el }
slots: [], // array of slot objects | null (length = activeDeal.qty)
products: [],
activeFilter: 'all'
};
this.cache = {};
this._initDeals();
this._bindUI();
}
/* ── Init ─────────────────────────────────────────────── */
_initDeals() {
const buttons = [...document.querySelectorAll('.bb-deal-btn')];
this.state.deals = buttons.map(btn => ({
el: btn,
qty: parseInt(btn.dataset.quantity) || 0,
collection: btn.dataset.collection,
originalPrice: parseFloat(btn.dataset.originalPrice) || 0,
finalPrice: parseFloat(btn.dataset.finalPrice) || 0,
title: btn.dataset.title,
savedPct: parseInt(btn.dataset.savedPct) || 0
}));
if (!this.state.deals.length) return;
// Auto-select the last deal (best value) by default
this._selectDeal(this.state.deals[this.state.deals.length - 1]);
// Always show the mobile footer
document.getElementById('bb-mobile-footer')?.classList.add('bb-mobile-footer--visible');
}
/* ── Deal Selection ───────────────────────────────────── */
_selectDeal(deal) {
const prev = this.state.activeDeal;
this.state.activeDeal = deal;
// Preserve filled slots when shrinking, expand with nulls when growing
const prevSlots = this.state.slots;
this.state.slots = Array.from({ length: deal.qty }, (_, i) => prevSlots[i] || null);
// Update headline
const titleEl = document.getElementById('bb-deal-title');
const perUnitEl = document.getElementById('bb-deal-per-unit');
if (titleEl) titleEl.textContent = `${deal.qty} pack for $${deal.finalPrice.toFixed(2)}`;
if (perUnitEl) perUnitEl.textContent = deal.qty > 0 ? `$${(deal.finalPrice / deal.qty).toFixed(2)} / shirt` : '';
// Highlight active button
document.querySelectorAll('.bb-deal-btn').forEach(b =>
b.classList.toggle('bb-deal-btn--active', b === deal.el)
);
// Fetch products (only if collection changed or first load)
if (!prev || prev.collection !== deal.collection) {
this._fetchProducts(deal.collection);
} else {
this._renderGrid();
}
this._renderSlots();
this._refreshBuyBtns();
}
/* ── Products ─────────────────────────────────────────── */
async _fetchProducts(handle) {
const list = document.getElementById('bb-product-list');
if (!handle) return;
if (this.cache[handle]) {
this._onLoaded(this.cache[handle]);
return;
}
list.innerHTML = '<div class="bb-loading"><div class="bb-spinner"></div></div>';
try {
const res = await fetch(`/collections/${handle}/products.json?limit=50`);
const data = await res.json();
this.cache[handle] = data.products || [];
this._onLoaded(this.cache[handle]);
} catch {
list.innerHTML = '<p class="bb-error">Could not load products. Please refresh the page.</p>';
}
}
_onLoaded(products) {
this.state.products = products;
this.state.activeFilter = 'all';
this._renderFilters();
this._renderGrid();
}
/* ── Filter Tabs ──────────────────────────────────────── */
_renderFilters() {
const nav = document.getElementById('bb-filter-tabs');
if (!nav) return;
const types = ['all', ...new Set(
this.state.products.map(p => p.product_type).filter(Boolean)
)];
// Only render tabs if there are multiple types
if (types.length <= 2) {
nav.innerHTML = '';
return;
}
nav.innerHTML = types.map(t => `
<button
class="bb-filter-tab ${this.state.activeFilter === t ? 'bb-filter-tab--active' : ''}"
data-filter="${this._esc(t)}"
>${t === 'all' ? 'All' : this._esc(t)}</button>
`).join('');
nav.querySelectorAll('.bb-filter-tab').forEach(btn => {
btn.addEventListener('click', () => {
this.state.activeFilter = btn.dataset.filter;
this._renderFilters();
this._renderGrid();
});
});
}
/* ── Product Grid ─────────────────────────────────────── */
_renderGrid() {
const list = document.getElementById('bb-product-list');
if (!list) return;
const isFull = this.state.slots.every(Boolean);
const filtered = this.state.activeFilter === 'all'
? this.state.products
: this.state.products.filter(p => p.product_type === this.state.activeFilter);
if (!filtered.length) {
list.innerHTML = '<p class="bb-error">No products found.</p>';
return;
}
list.innerHTML = `<div class="bb-product-grid">${filtered.map(p => this._cardHTML(p, isFull)).join('')}</div>`;
list.querySelectorAll('.bb-pcard__add-btn[data-pid]').forEach(btn => {
btn.addEventListener('click', () => {
const product = this.state.products.find(p => String(p.id) === btn.dataset.pid);
if (product) this._handleAdd(product);
});
});
list.querySelectorAll('.bb-pcard__details[data-handle]').forEach(btn => {
btn.addEventListener('click', () => {
window.open(`/products/${btn.dataset.handle}`, '_blank', 'noopener');
});
});
}
_cardHTML(p, isFull) {
const count = this.state.slots.filter(s => s?.productTitle === p.title).length;
const img = p.images?.[0]?.src || '';
const allSoldOut = p.variants.every(v => !v.available);
const disabled = (isFull && count === 0) || allSoldOut;
let btnLabel = 'Add';
let btnClass = 'bb-pcard__add-btn';
if (allSoldOut) { btnLabel = 'Sold Out'; btnClass += ' bb-pcard__add-btn--soldout'; }
else if (isFull) { btnLabel = 'Pack Full'; btnClass += ' bb-pcard__add-btn--full'; }
return `
<div class="bb-pcard" data-product-id="${p.id}">
<div class="bb-pcard__image">
${img ? `<img src="${img}" alt="${this._esc(p.title)}" loading="lazy">` : ''}
</div>
<div class="bb-pcard__info">
<p class="bb-pcard__name">${this._esc(p.title)}</p>
<hr class="bb-pcard__divider">
<button class="bb-pcard__details" data-handle="${p.handle}" type="button">See Details</button>
<div class="bb-pcard__add-wrap">
<button
class="${btnClass}"
type="button"
data-pid="${p.id}"
${disabled ? 'disabled' : ''}
>${btnLabel}</button>
${count > 0 ? `<span class="bb-pcard__count">${count}</span>` : ''}
</div>
</div>
</div>`;
}
/* ── Add / Remove Logic ───────────────────────────────── */
_handleAdd(product) {
if (this.state.slots.every(Boolean)) return;
const availableVariants = product.variants.filter(v => v.available);
if (!availableVariants.length) return;
if (availableVariants.length === 1 && product.variants.length === 1) {
// Single variant — add directly
const v = availableVariants[0];
this._addToSlot({
variantId: v.id,
productTitle: product.title,
variantTitle: v.title !== 'Default Title' ? v.title : '',
image: product.images?.[0]?.src || '',
handle: product.handle
});
} else {
// Multiple variants → open size drawer
this._openVariantDrawer(product);
}
}
_addToSlot(item) {
const idx = this.state.slots.indexOf(null);
if (idx === -1) return;
this.state.slots[idx] = item;
this._refreshUI();
}
_removeFromSlot(index) {
if (index < 0 || index >= this.state.slots.length) return;
this.state.slots[index] = null;
this._refreshUI();
}
_removeLastOfVariant(variantId) {
for (let i = this.state.slots.length - 1; i >= 0; i--) {
if (this.state.slots[i]?.variantId === variantId) {
this.state.slots[i] = null;
this._refreshUI();
return;
}
}
}
/* ── Refresh All UI ───────────────────────────────────── */
_refreshUI() {
this._renderSlots();
this._renderGrid();
this._renderMobileItems();
this._refreshBuyBtns();
}
/* ── Desktop Slots ────────────────────────────────────── */
_renderSlots() {
const el = document.getElementById('bb-slots');
if (!el) return;
el.innerHTML = this.state.slots.map((slot, i) => {
if (!slot) {
return `<div class="bb-slot bb-slot--empty">
<span class="bb-slot__placeholder">Shirt goes here!</span>
</div>`;
}
return `<div class="bb-slot bb-slot--filled">
${slot.image
? `<img class="bb-slot__thumb" src="${this._esc(slot.image)}" alt="${this._esc(slot.productTitle)}">`
: '<div class="bb-slot__thumb"></div>'}
<div class="bb-slot__info">
<div class="bb-slot__name">${this._esc(slot.productTitle)}</div>
${slot.variantTitle ? `<div class="bb-slot__variant">${this._esc(slot.variantTitle)}</div>` : ''}
</div>
<button class="bb-slot__remove" type="button" data-idx="${i}" aria-label="Remove">✕</button>
</div>`;
}).join('');
el.querySelectorAll('.bb-slot__remove').forEach(btn => {
btn.addEventListener('click', () => this._removeFromSlot(parseInt(btn.dataset.idx)));
});
}
/* ── Mobile Bundle List ───────────────────────────────── */
_renderMobileItems() {
const el = document.getElementById('bb-mobile-items');
if (!el) return;
// Group slots by variantId
const grouped = new Map();
this.state.slots.forEach(slot => {
if (!slot) return;
const key = slot.variantId;
const g = grouped.get(key);
if (g) {
g.count++;
} else {
grouped.set(key, { slot, count: 1 });
}
});
if (!grouped.size) {
el.innerHTML = '<p style="text-align:center;padding:16px;color:#999;font-size:13px">No items added yet</p>';
return;
}
el.innerHTML = [...grouped.entries()].map(([vid, { slot, count }]) => `
<div class="bb-mobile-item">
${slot.image
? `<img class="bb-mobile-item__thumb" src="${this._esc(slot.image)}" alt="${this._esc(slot.productTitle)}">`
: '<div class="bb-mobile-item__thumb"></div>'}
<div class="bb-mobile-item__label">
<div class="bb-mobile-item__name">${this._esc(slot.productTitle)}</div>
${slot.variantTitle ? `<div class="bb-mobile-item__variant">${this._esc(slot.variantTitle)}</div>` : ''}
</div>
<div class="bb-mobile-stepper">
<button class="bb-mobile-stepper__btn" data-vid="${vid}" data-action="dec" type="button">−</button>
<span class="bb-mobile-stepper__qty">${count}</span>
<button class="bb-mobile-stepper__btn" data-vid="${vid}" data-action="inc" type="button">+</button>
</div>
</div>
`).join('');
el.querySelectorAll('.bb-mobile-stepper__btn').forEach(btn => {
btn.addEventListener('click', () => {
const vid = parseInt(btn.dataset.vid);
if (btn.dataset.action === 'dec') {
this._removeLastOfVariant(vid);
} else {
if (!this.state.slots.every(Boolean)) {
const existing = this.state.slots.find(s => s?.variantId === vid);
if (existing) this._addToSlot({ ...existing });
}
}
});
});
}
/* ── Buy Buttons ──────────────────────────────────────── */
_refreshBuyBtns() {
const deal = this.state.activeDeal;
const filled = this.state.slots.filter(Boolean).length;
const total = deal?.qty || 0;
const isDone = filled === total && total > 0;
const remain = total - filled;
const label = isDone ? 'Add to Cart' : `Add ${remain} more`;
const mobileLabel = isDone ? 'Add To Cart' : `Add ${remain} more`;
const desktopBtn = document.getElementById('bb-add-to-cart-btn');
if (desktopBtn) {
desktopBtn.textContent = label;
desktopBtn.disabled = !isDone;
}
const mobileBtn = document.getElementById('bb-mobile-add-btn');
if (mobileBtn) {
mobileBtn.textContent = mobileLabel;
mobileBtn.disabled = !isDone;
}
const toggleText = document.getElementById('bb-mobile-toggle-text');
if (toggleText) {
toggleText.textContent = `View Your (${total} pack)`;
}
}
/* ── Variant Drawer ───────────────────────────────────── */
_openVariantDrawer(product) {
const title = document.getElementById('bb-drawer-title');
const body = document.getElementById('bb-drawer-body');
if (!body) return;
if (title) title.textContent = 'Select a Size';
body.innerHTML = `
<div class="bb-vdrawer__product">
${product.images?.[0]?.src
? `<img class="bb-vdrawer__img" src="${this._esc(product.images[0].src)}" alt="${this._esc(product.title)}">`
: ''}
<p class="bb-vdrawer__name">${this._esc(product.title)}</p>
</div>
<div class="bb-vdrawer__variants">
${product.variants.map(v => `
<button
class="bb-vdrawer__btn ${!v.available ? 'bb-vdrawer__btn--soldout' : ''}"
type="button"
data-vid="${v.id}"
data-vname="${this._esc(v.title)}"
${!v.available ? 'disabled' : ''}
>${this._esc(v.title)}</button>
`).join('')}
</div>`;
body.querySelectorAll('.bb-vdrawer__btn:not(.bb-vdrawer__btn--soldout)').forEach(btn => {
btn.addEventListener('click', () => {
const variantTitle = btn.dataset.vname !== 'Default Title' ? btn.dataset.vname : '';
this._addToSlot({
variantId: parseInt(btn.dataset.vid),
productTitle: product.title,
variantTitle,
image: product.images?.[0]?.src || '',
handle: product.handle
});
this._toggleDrawer(false);
});
});
this._toggleDrawer(true);
}
_toggleDrawer(open) {
document.getElementById('bb-drawer')?.classList.toggle('bb-drawer--open', open);
document.getElementById('bb-overlay')?.classList.toggle('bb-overlay--open', open);
document.body.style.overflow = open ? 'hidden' : '';
}
/* ── Add to Cart ──────────────────────────────────────── */
async _addToCart() {
const desktopBtn = document.getElementById('bb-add-to-cart-btn');
const mobileBtn = document.getElementById('bb-mobile-add-btn');
[desktopBtn, mobileBtn].forEach(b => {
if (b) { b.textContent = 'Adding\u2026'; b.disabled = true; }
});
const items = this.state.slots
.filter(Boolean)
.map(s => ({
id: s.variantId,
quantity: 1,
properties: { _bundle: this.state.activeDeal?.title || 'Bundle' }
}));
try {
const res = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items })
});
if (!res.ok) throw new Error('Cart error');
window.location.href = window.bbBundleData?.cartUrl || '/cart';
} catch {
alert('Sorry, something went wrong. Please try again.');
this._refreshBuyBtns();
}
}
/* ── UI Bindings ──────────────────────────────────────── */
_bindUI() {
// Deal picker clicks
document.getElementById('bb-deal-btns')?.addEventListener('click', e => {
const btn = e.target.closest('.bb-deal-btn');
if (!btn) return;
const deal = this.state.deals.find(d => d.el === btn);
if (deal) this._selectDeal(deal);
});
// Close drawer
document.getElementById('bb-drawer-close')?.addEventListener('click', () => this._toggleDrawer(false));
document.getElementById('bb-overlay')?.addEventListener('click', () => this._toggleDrawer(false));
// Add to cart
['bb-add-to-cart-btn', 'bb-mobile-add-btn'].forEach(id => {
document.getElementById(id)?.addEventListener('click', () => this._addToCart());
});
// Mobile bundle toggle
document.getElementById('bb-mobile-toggle')?.addEventListener('click', () => {
const bundle = document.getElementById('bb-mobile-bundle');
const chevron = document.getElementById('bb-mobile-toggle-chevron');
const toggle = document.getElementById('bb-mobile-toggle');
if (!bundle) return;
const isOpen = !bundle.hidden;
bundle.hidden = isOpen;
chevron?.classList.toggle('bb-mobile-toggle-chevron--open', !isOpen);
toggle?.setAttribute('aria-expanded', String(!isOpen));
if (!isOpen) this._renderMobileItems();
});
}
/* ── Helpers ──────────────────────────────────────────── */
_esc(str) {
return String(str || '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
}
/* ── Boot ─────────────────────────────────────────────────── */
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('bundle-builder')) {
window.bb = new BundleBuilder();
}
});
- Create the section named as
bundle-builder.liquidand paste the following code there:
{%- liquid
assign bundle_offers = shop.metaobjects['bundle_offer'].values
-%}
<link rel="stylesheet" href="{{ 'bundle-builder.css' | asset_url }}">
<div class="bundle-builder page-width" id="bundle-builder">
<div class="bb-header">
<h2 class="bb-header__title">{{ section.settings.heading }}</h2>
<p class="bb-header__sub">{{ section.settings.subheading }}</p>
<hr class="bb-header__rule">
</div>
<div class="bb-layout">
<!-- LEFT: Product Picker -->
<div class="bb-picker">
<nav class="bb-filter-tabs" id="bb-filter-tabs" aria-label="Product categories"></nav>
<div class="bb-product-list" id="bb-product-list">
<div class="bb-loading" id="bb-product-loading">
<div class="bb-spinner"></div>
</div>
</div>
</div>
<!-- RIGHT: Sticky Summary Panel -->
<aside class="bb-summary" id="bb-summary">
<!-- Deal Picker -->
<div class="bb-deal-picker">
<div class="bb-deal-headline">
<span class="bb-deal-headline__title" id="bb-deal-title">-</span>
<span class="bb-deal-headline__per" id="bb-deal-per-unit"></span>
</div>
<div class="bb-deal-btns" id="bb-deal-btns">
{%- for offer in bundle_offers -%}
{%- assign orig = offer.original_price.value | plus: 0.0 -%}
{%- assign final = offer.final_price.value | plus: 0.0 -%}
{%- assign qty = offer.quantity.value | plus: 0 -%}
{%- assign badge_color = offer.badge_color -%}
{%- if orig > 0 -%}
{%- assign saved_pct = orig | minus: final | divided_by: orig | times: 100 | round -%}
{%- else -%}
{%- assign saved_pct = 0 -%}
{%- endif -%}
<div class="bb-deal-btn-wrap">
{%- if offer.badge_text.value != blank -%}
<span class="bb-deal-badge" style="background-color: {{ badge_color }};">{{ offer.badge_text.value }}</span>
{%- elsif saved_pct > 0 -%}
<span class="bb-deal-badge" style="background-color: {{ badge_color }};">{{ saved_pct }}% OFF</span>
{%- else -%}
<span class="bb-deal-badge bb-deal-badge--ghost"></span>
{%- endif -%}
<button class="bb-deal-btn"
type="button"
data-quantity="{{ qty }}"
data-collection="{{ offer.collection.value.handle }}"
data-original-price="{{ orig }}"
data-final-price="{{ final }}"
data-title="{{ offer.title.value | escape }}"
data-saved-pct="{{ saved_pct }}">
{{ qty }} pack
</button>
</div>
{%- else -%}
<p style="color:#999;font-size:13px">No bundle offers found. Add <strong>bundle_offer</strong> metaobjects in Shopify admin.</p>
{%- endfor -%}
</div>
{%- if section.settings.note_text != blank -%}
<p class="bb-deal-shipping">{{ section.settings.note_text }}</p>
{%- endif -%}
</div>
<!-- Slots -->
<div class="bb-slots" id="bb-slots"></div>
<!-- Add to Cart CTA -->
<div class="bb-summary__footer">
<button class="bb-summary__add-btn" id="bb-add-to-cart-btn" type="button" disabled>
Add to Cart
</button>
</div>
</aside>
</div>
</div>
<!-- Mobile Sticky Footer -->
<div class="bb-mobile-footer" id="bb-mobile-footer">
<button class="bb-mobile-toggle" id="bb-mobile-toggle" type="button" aria-expanded="false">
<span id="bb-mobile-toggle-text">View Your Bundle</span>
<span class="bb-mobile-toggle-chevron" id="bb-mobile-toggle-chevron">∧</span>
</button>
<div class="bb-mobile-bundle" id="bb-mobile-bundle" hidden>
<div class="bb-mobile-items" id="bb-mobile-items"></div>
</div>
<button class="bb-mobile-add-btn" id="bb-mobile-add-btn" type="button" disabled>
Add To Cart
</button>
</div>
<!-- Variant / Size Drawer -->
<div class="bb-overlay" id="bb-overlay" aria-hidden="true"></div>
<div class="bb-drawer" id="bb-drawer" role="dialog" aria-modal="true">
<div class="bb-drawer__handle"></div>
<div class="bb-drawer__header">
<h3 class="bb-drawer__title" id="bb-drawer-title">Select a Size</h3>
<button class="bb-drawer__close" id="bb-drawer-close" type="button" aria-label="Close">✕</button>
</div>
<div class="bb-drawer__body" id="bb-drawer-body"></div>
</div>
<script>
window.bbBundleData = {
cartUrl: '{{ routes.cart_url }}',
moneyFormat: {{ shop.money_format | json }}
};
</script>
<script src="{{ 'bundle-builder.js' | asset_url }}" defer></script>
{% schema %}
{
"name": "Bundle Builder",
"tag": "section",
"settings": [
{ "type": "text", "id": "heading", "label": "Heading", "default": "Build Your Pack" },
{ "type": "textarea", "id": "subheading", "label": "Subheading", "default": "Create a custom bundle from our collection." },
{ "type": "text", "id": "note_text", "label": "Note","default": "Free shipping with 5 or 10 pack!" }
],
"presets": [{ "name": "Bundle Builder" }]
}
{% endschema %}
-
Go to the Shopify theme editor, in the pages create a new template and name it as
bundle-builderand then create a new page from Shopify admin panel -> Sales channels -> Online Store -> Pages and name it as bundle builder and assign the newly created template to it. -
Then go to Content Metaobjects -> Bundle Offer and setup the offers accordingly as shown in the image
-
Then Setup the automatic discounts as shown in the image
-
Then setup the automatic discounts for each bundle and collection accordingly
[!NOTE] Make sure that each item in the collection has the same price.
Video Tutorial
Watch the step-by-step video tutorial here:
Need Help With Your Shopify Store?
I'm a Shopify expert developer working as a solo specialist. When you reach out, you work directly with me, no project managers or middlemen.
If you need help with Shopify development, improving conversions, or increasing your store’s average order value, feel free to message me on WhatsApp for a quick response.
We can work through Upwork or you can pay securely via Payoneer.
Message me on WhatsApp