Extend the standard wishlist page with a per-card variant selector so shoppers can pick size and colour before adding to cart.
Wishlist Page - Apparel Extensions
Apparel shoppers often save items before deciding on a size or colour. This guide adds one thing to the base Wishlist Page implementation:
- Variant selector (
<select>) on each product card, built from Shopify's product JSON endpoint, letting the shopper pick a size or colour before adding to cart
The selected variant drives the cart add call. The selection is kept in a per-card currentEpi closure variable and is not persisted back to the wishlist.
Already have the base setup? Only
renderProductsneeds replacing -- see Modified renderProducts. Everything else (initWishlistPage,setupListSwitcher,setupShareButton, the share modal, save nudge, login nudge) is unchanged, including the sessionStorage cache optimisation:initWishlistPagerenders immediately from cache, thenswat.fetchListsalways re-renders from the authoritative API response. Starting from scratch? Copy the Complete Implementation directly -- it contains every file you need.
What Changes
| Base guide | Apparel extension | |
|---|---|---|
renderProducts | Async; fetches Shopify product JSON via productJSONCache; renders a cart and remove button per card | Also reads shopifyProduct.variants to build a variant <select> per card; tracks currentEpi per card so the cart add always uses the shopper's last-selected variant; re-evaluates stock state and cart button when the selection changes |
Nothing else in the base guide changes. productJSONCache, fetchProductJSON, initWishlistPage, setupListSwitcher, setupShareButton, the share modal, save nudge, and login nudge are all unchanged.
Modified renderProducts
renderProductsReplace renderProducts in assets/swym-wishlist-page.js with the version below. The code block is identical to the renderProducts function inside the Complete Implementation — no need to reconcile between them. The apparel version differs from the base guide in three ways:
-
Variant data --
allVariantsis read fromshopifyProduct.variants. The variant matchingproduct.episets the initial stock state, displayed price, and selected option.currentEpistarts atproduct.epiand updates as the shopper changes the select. -
Variant
<select>-- rendered only when the product has more than one variant. Out-of-stock options are disabled. The change handler updatescurrentEpi, re-evaluates stock, and syncs the cart button label and disabled state. -
Cart add uses
currentEpi-- the cart button listener always uses the closure-localcurrentEpi(notproduct.epi), so the shopper gets whichever variant they last selected. The remove call still uses the originalproduct.epibecause that is how the item is keyed in the Swym list.
// Replaces renderProducts in the base wishlist-page.js
async function renderProducts(swat, lid, products, container, countEl, emptyEl) {
container.setAttribute('aria-busy', 'true');
container.innerHTML = '';
if (!products || products.length === 0) {
if (emptyEl) emptyEl.style.display = 'block';
container.setAttribute('aria-busy', 'false');
return;
}
if (emptyEl) emptyEl.style.display = 'none';
var shopifyData = await Promise.all(products.map(function(product) {
var key = String(product.epi);
if (productJSONCache[key]) return Promise.resolve(productJSONCache[key]);
return fetchProductJSON(product.du)
.then(function(data) { productJSONCache[key] = data; return data; })
.catch(function() { return null; });
}));
products.forEach(function(product, index) {
var shopifyProduct = shopifyData[index];
var allVariants = shopifyProduct && shopifyProduct.variants ? shopifyProduct.variants : null;
var savedVariant = allVariants
? allVariants.find(function(v) { return String(v.id) === String(product.epi); })
: null;
var inStock = savedVariant ? savedVariant.available : product.stk !== 0;
var displayPrice = savedVariant ? savedVariant.price : product.pr;
var currentEpi = product.epi;
var title = esc(shopifyProduct && shopifyProduct.title || product.dt || 'Product');
var href = esc(shopifyProduct && shopifyProduct.url || product.du || '#');
var imgSrc = esc(shopifyProduct && (shopifyProduct.featured_image || (shopifyProduct.images && shopifyProduct.images[0])) || product.iu || '');
var variantSelectId = 'swym-variant-' + esc(String(product.epi));
var card = document.createElement('div');
card.className = 'swym-wishlist-card';
card.innerHTML =
'<a href="' + href + '">' +
'<img src="' + imgSrc + '" alt="' + title + '" loading="lazy">' +
'</a>' +
'<a href="' + href + '">' + title + '</a>' +
(displayPrice ? '<span>' + esc(formatPrice(displayPrice)) + '</span>' : '') +
'<div class="swym-wishlist-card-actions">' +
'<label class="swym-wishlist-variant-label"' +
(allVariants && allVariants.length > 1 ? ' for="' + variantSelectId + '"' : '') + '>' +
(allVariants && allVariants.length > 1 ? 'Variant' : ' ') +
'</label>' +
(allVariants && allVariants.length > 1
? '<select id="' + variantSelectId + '" class="swym-wishlist-variant-select"' +
' aria-label="Select variant for ' + title + '"></select>'
: '<span class="swym-wishlist-variant-static">' +
(savedVariant && savedVariant.title && savedVariant.title !== 'Default Title'
? esc(savedVariant.title) : '') +
'</span>') +
'<button type="button" class="swym-wishlist-cart-btn"' + (!inStock ? ' disabled' : '') +
' aria-label="' + (inStock ? 'Add ' + title + ' to cart' : title + ' is out of stock') + '">' +
(inStock ? 'Add to Cart' : 'Out of Stock') +
'</button>' +
'<button type="button" class="swym-wishlist-remove-btn" aria-label="Remove ' + title + ' from wishlist">×</button>' +
'</div>';
var variantSelect = card.querySelector('.swym-wishlist-variant-select');
var cartBtn = card.querySelector('.swym-wishlist-cart-btn');
var removeBtn = card.querySelector('.swym-wishlist-remove-btn');
if (variantSelect && allVariants && allVariants.length > 1) {
allVariants.forEach(function(variant) {
var option = document.createElement('option');
option.value = variant.id;
option.textContent = variant.title + (variant.available ? '' : ' (Out of Stock)');
option.selected = String(variant.id) === String(product.epi);
option.disabled = !variant.available;
variantSelect.appendChild(option);
});
variantSelect.addEventListener('change', function() {
currentEpi = Number(variantSelect.value);
var selected = allVariants.find(function(v) { return String(v.id) === String(currentEpi); });
var nowInStock = selected && selected.available;
cartBtn.disabled = !nowInStock;
cartBtn.textContent = nowInStock ? 'Add to Cart' : 'Out of Stock';
cartBtn.setAttribute('aria-label', nowInStock
? 'Add ' + title + ' to cart'
: title + ' is out of stock');
});
}
cartBtn.addEventListener('click', function() {
if (cartBtn.disabled) return;
cartBtn.textContent = 'Adding...';
cartBtn.setAttribute('aria-label', 'Adding ' + title + ' to cart');
cartBtn.disabled = true;
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: [{ id: currentEpi, quantity: 1 }] })
})
.then(function(response) {
if (response.ok) {
cartBtn.textContent = 'Added!';
cartBtn.setAttribute('aria-label', title + ' added to cart');
setTimeout(function() {
cartBtn.textContent = 'Add to Cart';
cartBtn.setAttribute('aria-label', 'Add ' + title + ' to cart');
cartBtn.disabled = false;
}, 2000);
} else {
cartBtn.textContent = 'Error';
cartBtn.setAttribute('aria-label', 'Failed to add ' + title + ' to cart');
cartBtn.disabled = false;
}
})
.catch(function() {
cartBtn.textContent = 'Error';
cartBtn.setAttribute('aria-label', 'Failed to add ' + title + ' to cart');
cartBtn.disabled = false;
});
});
removeBtn.addEventListener('click', function() {
swat.deleteFromList(
lid,
{ epi: product.epi, empi: product.empi, du: product.du },
function() {
card.remove();
var remaining = container.querySelectorAll('.swym-wishlist-card');
if (countEl) countEl.textContent = '(' + remaining.length + ' items)';
if (remaining.length === 0 && emptyEl) emptyEl.style.display = 'block';
},
function() {}
);
});
container.appendChild(card);
});
container.setAttribute('aria-busy', 'false');
}
currentEpivsproduct.epi:product.epiis the variant ID stored in the wishlist.currentEpistarts at that value and updates as the shopper changes the select. The cart add call always usescurrentEpi, so the shopper gets whichever variant they last selected - not necessarily the one originally saved.
Remove uses
product.epi: The remove call always references the originalproduct.epibecause that is how the item is keyed in the Swym list. Changing the variant select does not change which item gets removed.
Styling additions
| Element | Purpose |
|---|---|
.swym-wishlist-variant-label | Label above the variant dropdown - small, muted; use theme body font |
.swym-wishlist-variant-select | Variant <select> - full card width; inherits theme font and border style |
.swym-wishlist-cart-btn | Add to Cart button; disabled state when out of stock or no in-stock variant is selected |
.swym-wishlist-remove-btn | Remove from wishlist button; ghost or icon style to keep visual weight low |
Product Fields and APIs Used (additions)
| Field / API | Usage |
|---|---|
product.epi | Variant ID stored on the wishlist item - used as the initial value of currentEpi and as the initially selected <option> |
variants[n].id | Shopify variant ID - set as the <option> value and passed to /cart/add.js via currentEpi |
variants[n].available | Stock state - disables out-of-stock options and keeps the Add to Cart button in sync |
Complete Implementation
Copy the files for your setup into the theme. The Liquid goes in sections/, the JS and CSS in assets/.
sections/swym-wishlist-page.liquid
sections/swym-wishlist-page.liquid{% comment %} Swym Wishlist Page — Apparel {% endcomment %}
<div id="swym-wishlist-page" class="page-width">
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
<h1><span id="swym-wishlist-title">My Wishlist</span> <span id="swym-wishlist-count"></span></h1>
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<div id="swym-list-switcher-wrapper" style="display:none;">
<label for="swym-list-switcher" style="font-size:0.875rem;">List:</label>
<select id="swym-list-switcher" aria-label="Switch between your wishlists"></select>
</div>
<button id="swym-share-btn" style="display:none;" aria-label="Share wishlist">Share Wishlist</button>
</div>
</div>
{%- comment -%} Share modal — rendered hidden; JS controls visibility {%- endcomment -%}
<div id="swym-share-modal" role="dialog" aria-modal="true" aria-labelledby="swym-share-modal-title" style="display:none; position:fixed; inset:0; z-index:9999;">
<div id="swym-share-modal-overlay" style="display:block; position:absolute; inset:0; background:rgba(0,0,0,0.5);"></div>
<div id="swym-share-modal-content" style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; padding:2rem; max-width:480px; width:90%; border-radius:8px; max-height:90vh; overflow-y:auto;">
<div id="swym-share-modal-header">
<h2 id="swym-share-modal-title">Share Your Wishlist</h2>
<button id="swym-share-modal-close" aria-label="Close share dialog">×</button>
</div>
{%- comment -%} Email share form {%- endcomment -%}
<div class="swym-share-section">
<label for="swym-share-name-input">Name</label>
<input id="swym-share-name-input" type="text" placeholder="Your name" aria-label="Your name">
<label for="swym-share-email-input">Email</label>
<input id="swym-share-email-input" type="email" placeholder="[email protected]" aria-label="Recipient email address">
<label for="swym-share-note-input">Message</label>
<textarea id="swym-share-note-input" placeholder="Check out my wishlist!" aria-label="Message"></textarea>
<button id="swym-share-email-btn" aria-label="Send wishlist by email">Share Via Email</button>
<span id="swym-share-email-feedback" style="display:none;" aria-live="polite">Email sent!</span>
</div>
{%- comment -%} Social / copy {%- endcomment -%}
<div class="swym-share-section swym-share-social-section">
<p class="swym-share-divider-label">or Share via</p>
<div class="swym-share-social-row">
<button class="swym-share-social-btn" data-platform="facebook" aria-label="Share on Facebook">Facebook</button>
<button class="swym-share-social-btn" data-platform="twitter" aria-label="Share on Twitter / X">X</button>
<button id="swym-share-copy-btn" aria-label="Copy link">Copy Link</button>
</div>
<span id="swym-share-copy-feedback" style="display:none;" aria-live="polite">Copied!</span>
</div>
</div>
</div>
{%- comment -%} Login nudge for anonymous users {%- endcomment -%}
<div id="swym-login-nudge" style="display:none;">
<p>Sign in to save your wishlist across devices</p>
<a href="/account/login?return_url=/pages/wishlist">Sign in</a>
</div>
{%- comment -%} Save nudge — shown after 3+ items for anonymous users {%- endcomment -%}
<div id="swym-save-nudge" style="display:none;" aria-live="polite" aria-atomic="true">
<p><strong>Don't lose your list!</strong> You've saved <span id="swym-save-nudge-count"></span> items. <a href="/account/login?return_url=/pages/wishlist">Sign in to keep them.</a></p>
<button id="swym-save-nudge-dismiss" aria-label="Dismiss">×</button>
</div>
<div id="swym-wishlist-loading" aria-live="polite" aria-atomic="true">Loading your wishlist...</div>
<div id="swym-wishlist-empty" style="display:none;">
<p>Your wishlist is empty</p>
<p>Start adding items you love</p>
<a href="/collections">Browse products</a>
</div>
<div id="swym-wishlist-grid" aria-busy="true" aria-label="Wishlist products"></div>
</div>
<script src="{{ 'swym-wishlist-page.js' | asset_url }}" defer></script>assets/swym-wishlist-page.js
assets/swym-wishlist-page.js(function() {
'use strict';
var SAVE_NUDGE_THRESHOLD = 3;
var SS_PRODUCTS_KEY = 'swym-wishlist-products';
var SS_SHARE_URL_KEY = 'swym-share-url';
var productJSONCache = {};
var shareButtonHandler = null;
function initWishlistPage(swat) {
var isLoggedIn = swat.platform.isLoggedIn();
var grid = document.getElementById('swym-wishlist-grid');
var loading = document.getElementById('swym-wishlist-loading');
var empty = document.getElementById('swym-wishlist-empty');
var countEl = document.getElementById('swym-wishlist-count');
if (!grid) return;
if (!isLoggedIn) {
var loginNudge = document.getElementById('swym-login-nudge');
if (loginNudge) loginNudge.style.display = '';
}
var cachedRaw = sessionStorage.getItem(SS_PRODUCTS_KEY);
var cachedData = null;
if (cachedRaw) {
try {
cachedData = JSON.parse(cachedRaw);
if (loading) loading.style.display = 'none';
renderProducts(swat, cachedData.lid, cachedData.products, grid, countEl, empty);
if (countEl) countEl.textContent = '(' + cachedData.products.length + ' items)';
setupShareButton(swat, cachedData.lid, cachedData.products.length);
} catch (e) {
sessionStorage.removeItem(SS_PRODUCTS_KEY);
cachedData = null;
}
}
swat.fetchLists({
callbackFn: function(lists) {
if (loading) loading.style.display = 'none';
var cachedLid = cachedData ? cachedData.lid : null;
var activeList = lists && lists.length
? (cachedLid ? (lists.find(function(l) { return l.lid === cachedLid; }) || lists[0]) : lists[0])
: null;
var lid = activeList ? activeList.lid : null;
var products = activeList ? (activeList.listcontents || []) : [];
sessionStorage.setItem(SS_PRODUCTS_KEY, JSON.stringify({ lid: lid, products: products }));
if (products.length === 0) {
if (empty) empty.style.display = 'block';
if (countEl) countEl.textContent = '(0 items)';
} else {
if (countEl) countEl.textContent = '(' + products.length + ' items)';
showSaveNudge(swat, products.length);
renderProducts(swat, lid, products, grid, countEl, empty);
setupShareButton(swat, lid, products.length);
sessionStorage.removeItem(SS_SHARE_URL_KEY);
}
setupListSwitcher(swat, lists, lid, grid, countEl, empty);
if (swat.evtLayer) {
swat.evtLayer.addEventListener('sw:removedfromwishlist', function() {
swat.fetchLists({
callbackFn: function(freshLists) {
var activeLid = sessionStorage.getItem(SS_PRODUCTS_KEY)
? (JSON.parse(sessionStorage.getItem(SS_PRODUCTS_KEY)) || {}).lid
: lid;
var activeList = freshLists && freshLists.length
? (freshLists.find(function(l) { return l.lid === activeLid; }) || freshLists[0])
: null;
var currentLid = activeList ? activeList.lid : null;
var updated = activeList ? (activeList.listcontents || []) : [];
var count = updated.length;
sessionStorage.setItem(SS_PRODUCTS_KEY, JSON.stringify({ lid: currentLid, products: updated }));
sessionStorage.removeItem(SS_SHARE_URL_KEY);
renderProducts(swat, currentLid, updated, grid, countEl, empty);
if (countEl) countEl.textContent = '(' + count + ' items)';
setupShareButton(swat, currentLid, count);
setupListSwitcher(swat, freshLists || [], currentLid, grid, countEl, empty);
}
});
});
}
},
errorFn: function() {
if (loading) loading.style.display = 'none';
}
});
}
function showSaveNudge(swat, count) {
if (swat.platform.isLoggedIn()) return;
if (count < SAVE_NUDGE_THRESHOLD) return;
if (sessionStorage.getItem('swym-save-nudge-dismissed')) return;
var nudge = document.getElementById('swym-save-nudge');
if (!nudge) return;
var countSpan = document.getElementById('swym-save-nudge-count');
if (countSpan) countSpan.textContent = count;
nudge.style.display = '';
var dismiss = document.getElementById('swym-save-nudge-dismiss');
if (dismiss) {
dismiss.addEventListener('click', function() {
nudge.style.display = 'none';
sessionStorage.setItem('swym-save-nudge-dismissed', 'true');
});
}
}
// IMPORTANT: Replace the HTML below with your theme's collection card structure
// to match the look and feel of your collection page
async function renderProducts(swat, lid, products, container, countEl, emptyEl) {
container.setAttribute('aria-busy', 'true');
container.innerHTML = '';
if (!products || products.length === 0) {
if (emptyEl) emptyEl.style.display = 'block';
container.setAttribute('aria-busy', 'false');
return;
}
if (emptyEl) emptyEl.style.display = 'none';
var shopifyData = await Promise.all(products.map(function(product) {
var key = String(product.epi);
if (productJSONCache[key]) return Promise.resolve(productJSONCache[key]);
return fetchProductJSON(product.du)
.then(function(data) { productJSONCache[key] = data; return data; })
.catch(function() { return null; });
}));
products.forEach(function(product, index) {
var shopifyProduct = shopifyData[index];
var allVariants = shopifyProduct && shopifyProduct.variants ? shopifyProduct.variants : null;
var savedVariant = allVariants
? allVariants.find(function(v) { return String(v.id) === String(product.epi); })
: null;
var inStock = savedVariant ? savedVariant.available : product.stk !== 0;
var displayPrice = savedVariant ? savedVariant.price : product.pr;
var currentEpi = product.epi;
var title = esc(shopifyProduct && shopifyProduct.title || product.dt || 'Product');
var href = esc(shopifyProduct && shopifyProduct.url || product.du || '#');
var imgSrc = esc(shopifyProduct && (shopifyProduct.featured_image || (shopifyProduct.images && shopifyProduct.images[0])) || product.iu || '');
var variantSelectId = 'swym-variant-' + esc(String(product.epi));
var card = document.createElement('div');
card.className = 'swym-wishlist-card';
card.innerHTML =
'<a href="' + href + '">' +
'<img src="' + imgSrc + '" alt="' + title + '" loading="lazy">' +
'</a>' +
'<a href="' + href + '">' + title + '</a>' +
(displayPrice ? '<span>' + esc(formatPrice(displayPrice)) + '</span>' : '') +
'<div class="swym-wishlist-card-actions">' +
'<label class="swym-wishlist-variant-label"' +
(allVariants && allVariants.length > 1 ? ' for="' + variantSelectId + '"' : '') + '>' +
(allVariants && allVariants.length > 1 ? 'Variant' : ' ') +
'</label>' +
(allVariants && allVariants.length > 1
? '<select id="' + variantSelectId + '" class="swym-wishlist-variant-select"' +
' aria-label="Select variant for ' + title + '"></select>'
: '<span class="swym-wishlist-variant-static">' +
(savedVariant && savedVariant.title && savedVariant.title !== 'Default Title'
? esc(savedVariant.title) : '') +
'</span>') +
'<button type="button" class="swym-wishlist-cart-btn"' + (!inStock ? ' disabled' : '') +
' aria-label="' + (inStock ? 'Add ' + title + ' to cart' : title + ' is out of stock') + '">' +
(inStock ? 'Add to Cart' : 'Out of Stock') +
'</button>' +
'<button type="button" class="swym-wishlist-remove-btn" aria-label="Remove ' + title + ' from wishlist">×</button>' +
'</div>';
var variantSelect = card.querySelector('.swym-wishlist-variant-select');
var cartBtn = card.querySelector('.swym-wishlist-cart-btn');
var removeBtn = card.querySelector('.swym-wishlist-remove-btn');
if (variantSelect && allVariants && allVariants.length > 1) {
allVariants.forEach(function(variant) {
var option = document.createElement('option');
option.value = variant.id;
option.textContent = variant.title + (variant.available ? '' : ' (Out of Stock)');
option.selected = String(variant.id) === String(product.epi);
option.disabled = !variant.available;
variantSelect.appendChild(option);
});
variantSelect.addEventListener('change', function() {
currentEpi = Number(variantSelect.value);
var selected = allVariants.find(function(v) { return String(v.id) === String(currentEpi); });
var nowInStock = selected && selected.available;
cartBtn.disabled = !nowInStock;
cartBtn.textContent = nowInStock ? 'Add to Cart' : 'Out of Stock';
cartBtn.setAttribute('aria-label', nowInStock
? 'Add ' + title + ' to cart'
: title + ' is out of stock');
});
}
cartBtn.addEventListener('click', function() {
if (cartBtn.disabled) return;
cartBtn.textContent = 'Adding...';
cartBtn.setAttribute('aria-label', 'Adding ' + title + ' to cart');
cartBtn.disabled = true;
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: [{ id: currentEpi, quantity: 1 }] })
})
.then(function(response) {
if (response.ok) {
cartBtn.textContent = 'Added!';
cartBtn.setAttribute('aria-label', title + ' added to cart');
setTimeout(function() {
cartBtn.textContent = 'Add to Cart';
cartBtn.setAttribute('aria-label', 'Add ' + title + ' to cart');
cartBtn.disabled = false;
}, 2000);
} else {
cartBtn.textContent = 'Error';
cartBtn.setAttribute('aria-label', 'Failed to add ' + title + ' to cart');
cartBtn.disabled = false;
}
})
.catch(function() {
cartBtn.textContent = 'Error';
cartBtn.setAttribute('aria-label', 'Failed to add ' + title + ' to cart');
cartBtn.disabled = false;
});
});
removeBtn.addEventListener('click', function() {
swat.deleteFromList(
lid,
{ epi: product.epi, empi: product.empi, du: product.du },
function() {
card.remove();
var remaining = container.querySelectorAll('.swym-wishlist-card');
if (countEl) countEl.textContent = '(' + remaining.length + ' items)';
if (remaining.length === 0 && emptyEl) emptyEl.style.display = 'block';
},
function() {}
);
});
container.appendChild(card);
});
container.setAttribute('aria-busy', 'false');
}
function bindShareUrlButtons(modal, copyBtn, url, swat, lid) {
if (copyBtn) {
copyBtn.disabled = false;
var freshCopy = copyBtn.cloneNode(true);
copyBtn.parentNode.replaceChild(freshCopy, copyBtn);
freshCopy.addEventListener('click', function() { handleCopyLink(url); });
}
modal.querySelectorAll('.swym-share-social-btn').forEach(function(socialBtn) {
var freshSocial = socialBtn.cloneNode(true);
socialBtn.parentNode.replaceChild(freshSocial, socialBtn);
freshSocial.addEventListener('click', function() {
handleSocialShare(swat, lid, freshSocial.dataset.platform, url);
});
});
}
function setupShareButton(swat, lid, productCount) {
var btn = document.getElementById('swym-share-btn');
if (!btn || !lid) return;
if (productCount === 0) {
btn.style.display = 'none';
return;
}
btn.style.display = '';
if (shareButtonHandler) btn.removeEventListener('click', shareButtonHandler);
shareButtonHandler = function() { openShareModal(swat, lid); };
btn.addEventListener('click', shareButtonHandler);
}
function openShareModal(swat, lid) {
var modal = document.getElementById('swym-share-modal');
if (!modal) return;
var nameInput = document.getElementById('swym-share-name-input');
var emailInput = document.getElementById('swym-share-email-input');
var noteInput = document.getElementById('swym-share-note-input');
var copyBtn = document.getElementById('swym-share-copy-btn');
if (nameInput) nameInput.value = '';
if (emailInput) emailInput.value = '';
if (noteInput) noteInput.value = '';
if (copyBtn) copyBtn.disabled = true;
modal.querySelectorAll('[id$="-feedback"]').forEach(function(el) { el.style.display = 'none'; });
modal.style.display = '';
document.body.style.overflow = 'hidden';
if (nameInput) nameInput.focus();
var cachedShareUrl = sessionStorage.getItem(SS_SHARE_URL_KEY);
if (cachedShareUrl) {
bindShareUrlButtons(modal, copyBtn, cachedShareUrl, swat, lid);
} else {
swat.markListPublic(lid, function() {
swat.generateSharedListURL(lid, function(url) {
sessionStorage.setItem(SS_SHARE_URL_KEY, url);
bindShareUrlButtons(modal, copyBtn, url, swat, lid);
});
}, function() {
if (copyBtn) copyBtn.disabled = true;
});
}
var emailBtn = document.getElementById('swym-share-email-btn');
if (emailBtn) {
var freshEmail = emailBtn.cloneNode(true);
emailBtn.parentNode.replaceChild(freshEmail, emailBtn);
freshEmail.addEventListener('click', function() { handleEmailShare(swat, lid); });
}
var closeBtn = document.getElementById('swym-share-modal-close');
var overlay = document.getElementById('swym-share-modal-overlay');
if (closeBtn) {
var freshClose = closeBtn.cloneNode(true);
closeBtn.parentNode.replaceChild(freshClose, closeBtn);
freshClose.addEventListener('click', closeShareModal);
}
if (overlay) {
var freshOverlay = overlay.cloneNode(true);
overlay.parentNode.replaceChild(freshOverlay, overlay);
freshOverlay.addEventListener('click', closeShareModal);
}
document.addEventListener('keydown', handleShareModalKeydown);
}
function handleShareModalKeydown(e) {
if (e.key === 'Escape') {
closeShareModal();
return;
}
if (e.key !== 'Tab') return;
var modal = document.getElementById('swym-share-modal-content');
if (!modal) return;
var focusable = Array.prototype.slice.call(
modal.querySelectorAll('a[href],button:not([disabled]),input,textarea,select,[tabindex]:not([tabindex="-1"])')
).filter(function(el) { return !el.disabled && el.offsetParent !== null; });
if (!focusable.length) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
} else {
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
}
}
function closeShareModal() {
var modal = document.getElementById('swym-share-modal');
if (modal) modal.style.display = 'none';
document.body.style.overflow = '';
document.removeEventListener('keydown', handleShareModalKeydown);
var trigger = document.getElementById('swym-share-btn');
if (trigger) trigger.focus();
}
function handleCopyLink(url) {
var feedback = document.getElementById('swym-share-copy-feedback');
var done = function() {
showModalFeedback(feedback);
setTimeout(closeShareModal, 800);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(done).catch(function() {
fallbackCopy(url, feedback);
setTimeout(closeShareModal, 800);
});
} else {
fallbackCopy(url, feedback);
setTimeout(closeShareModal, 800);
}
}
function handleEmailShare(swat, lid) {
var nameInput = document.getElementById('swym-share-name-input');
var emailInput = document.getElementById('swym-share-email-input');
var noteInput = document.getElementById('swym-share-note-input');
var emailBtn = document.getElementById('swym-share-email-btn');
var feedback = document.getElementById('swym-share-email-feedback');
if (!emailInput) return;
var email = emailInput.value.trim();
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
emailInput.focus();
return;
}
if (emailBtn) emailBtn.disabled = true;
swat.sendListViaEmail({
toEmailId: email,
note: noteInput ? noteInput.value.trim() : '',
fromName: nameInput ? nameInput.value.trim() : '',
lid: lid
}, function() {
if (emailBtn) emailBtn.disabled = false;
if (nameInput) nameInput.value = '';
emailInput.value = '';
if (noteInput) noteInput.value = '';
showModalFeedback(feedback);
setTimeout(closeShareModal, 1000);
}, function() {
if (emailBtn) emailBtn.disabled = false;
});
}
function handleSocialShare(swat, lid, platform, shareUrl) {
var platformUrlTemplate = '';
if (platform === 'facebook') platformUrlTemplate = 'https://www.facebook.com/sharer.php?u=' + encodeURIComponent(shareUrl) + '&t=';
else if (platform === 'twitter') platformUrlTemplate = 'https://twitter.com/share?text=&url=' + encodeURIComponent(shareUrl);
swat.shareListSocial(lid, '', platformUrlTemplate, platform, '', function() {});
if (platformUrlTemplate) window.open(platformUrlTemplate, '_blank', 'noopener,noreferrer');
closeShareModal();
}
function showModalFeedback(el) {
if (!el) return;
el.style.display = '';
setTimeout(function() { el.style.display = 'none'; }, 2500);
}
function fallbackCopy(url, feedbackEl) {
var ta = document.createElement('textarea');
ta.value = url;
ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none;';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); showModalFeedback(feedbackEl); } catch (e) {}
document.body.removeChild(ta);
}
function formatPrice(priceInCents) {
return '$' + (priceInCents / 100).toFixed(2);
}
function esc(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function fetchProductJSON(du) {
var parts = du.split('/products/');
if (parts.length < 2) return Promise.reject(new Error('invalid product URL'));
var url = window.location.origin + window.Shopify.routes.root + 'products/' + parts[1].split('?')[0] + '.js';
return fetch(url)
.then(function(response) {
if (!response.ok) throw new Error('product fetch failed');
return response.json();
});
}
function setWishlistTitle(name) {
var titleEl = document.getElementById('swym-wishlist-title');
if (titleEl) titleEl.textContent = name || 'My Wishlist';
}
function setupListSwitcher(swat, lists, activeLid, grid, countEl, emptyEl) {
var wrapper = document.getElementById('swym-list-switcher-wrapper');
var switcher = document.getElementById('swym-list-switcher');
if (!wrapper || !switcher) return;
if (!swat.isCollectionsEnabled() || lists.length < 2) {
wrapper.style.display = 'none';
return;
}
wrapper.style.display = '';
var fresh = switcher.cloneNode(false);
switcher.parentNode.replaceChild(fresh, switcher);
lists.forEach(function(list) {
var option = document.createElement('option');
option.value = list.lid;
option.textContent = list.lname + ' (' + list.listcontents.length + ' items)';
if (list.lid === activeLid) option.selected = true;
fresh.appendChild(option);
});
var activeList = lists.find(function(l) { return l.lid === activeLid; });
if (activeList) setWishlistTitle(activeList.lname);
fresh.addEventListener('change', function() {
var selected = lists.find(function(l) { return l.lid === fresh.value; });
if (!selected) return;
var products = selected.listcontents || [];
sessionStorage.setItem(SS_PRODUCTS_KEY, JSON.stringify({ lid: selected.lid, products: products }));
sessionStorage.removeItem(SS_SHARE_URL_KEY);
var count = products.length;
if (countEl) countEl.textContent = '(' + count + ' items)';
setWishlistTitle(selected.lname);
setupShareButton(swat, selected.lid, count);
renderProducts(swat, selected.lid, products, grid, countEl, emptyEl);
});
}
window.SwymCallbacks = window.SwymCallbacks || [];
window.SwymCallbacks.push(initWishlistPage);
})();As with all card styles, pull border colour, border radius, and font values from your theme's CSS variables rather than hardcoding them.
assets/swym-wishlist-page.css
assets/swym-wishlist-page.css/* ============================================================
Swym Wishlist Page — Apparel
============================================================ */
/* --- List switcher ---------------------------------------- */
#swym-list-switcher-wrapper {
display: flex;
align-items: center;
gap: 6px;
}
#swym-list-switcher-wrapper label {
font-size: 1rem;
color: #374151;
white-space: nowrap;
}
#swym-list-switcher {
padding: 7px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
background: #fff;
cursor: pointer;
}
#swym-list-switcher:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* --- Share button ----------------------------------------- */
#swym-share-btn {
padding: 9px 18px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
#swym-share-btn:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
/* --- Loading / empty states ------------------------------- */
#swym-wishlist-loading {
padding: 3rem 0;
text-align: center;
color: #6b7280;
font-size: 1.0625rem;
}
#swym-wishlist-empty {
padding: 4rem 0;
text-align: center;
}
#swym-wishlist-empty p {
margin: 0 0 0.5rem;
color: #374151;
}
#swym-wishlist-empty p:first-child {
font-size: 1.25rem;
font-weight: 600;
}
#swym-wishlist-empty p:last-of-type {
color: #6b7280;
font-size: 1rem;
margin-bottom: 1.5rem;
}
#swym-wishlist-empty a {
display: inline-block;
padding: 11px 22px;
background: #111827;
color: #fff;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
text-decoration: none;
transition: background 0.15s;
}
#swym-wishlist-empty a:hover {
background: #374151;
}
/* --- Nudges ----------------------------------------------- */
#swym-login-nudge {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 18px;
margin-bottom: 1.5rem;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
font-size: 1rem;
color: #0369a1;
}
#swym-login-nudge p {
margin: 0;
flex: 1;
}
#swym-login-nudge a {
font-weight: 600;
color: #0369a1;
text-decoration: underline;
}
#swym-save-nudge {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 13px 18px;
margin-bottom: 1.5rem;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
font-size: 1rem;
color: #92400e;
}
#swym-save-nudge p {
margin: 0;
flex: 1;
line-height: 1.5;
}
#swym-save-nudge a {
color: #92400e;
font-weight: 600;
}
#swym-save-nudge-dismiss {
flex-shrink: 0;
padding: 0 4px;
background: none;
border: none;
font-size: 1.375rem;
line-height: 1;
cursor: pointer;
color: #92400e;
opacity: 0.7;
}
#swym-save-nudge-dismiss:hover {
opacity: 1;
}
/* --- Product grid ----------------------------------------- */
#swym-wishlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
align-items: start;
gap: 1.5rem;
margin-top: 2rem;
}
#swym-wishlist-grid[aria-busy="true"] {
min-height: 600px;
}
@media (max-width: 640px) {
#swym-wishlist-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
}
/* --- Product card ----------------------------------------- */
.swym-wishlist-card {
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 10px;
overflow: hidden;
background: #fff;
transition: box-shadow 0.15s;
}
.swym-wishlist-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.swym-wishlist-card > a:first-child {
display: block;
aspect-ratio: 1 / 1;
overflow: hidden;
background: #f9fafb;
}
.swym-wishlist-card > a:first-child img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.2s;
}
.swym-wishlist-card:hover > a:first-child img {
transform: scale(1.03);
}
.swym-wishlist-card > a:nth-child(2) {
display: block;
padding: 14px 14px 4px;
font-size: 1rem;
font-weight: 500;
color: #111827;
text-decoration: none;
line-height: 1.4;
}
.swym-wishlist-card > a:nth-child(2):hover {
text-decoration: underline;
}
.swym-wishlist-card > span {
padding: 4px 14px 0;
font-size: 1.0625rem;
font-weight: 600;
color: #111827;
}
/* --- Card actions ----------------------------------------- */
.swym-wishlist-card-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 14px 14px;
margin-top: auto;
}
/* --- Variant selector ------------------------------------- */
.swym-wishlist-variant-label {
flex-basis: 100%;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: -2px;
}
.swym-wishlist-variant-select {
flex: 1 0 100%;
box-sizing: border-box;
height: 38px;
padding: 0 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.9375rem;
background: #fff;
color: #111827;
cursor: pointer;
appearance: auto;
}
.swym-wishlist-variant-select:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* Exact same height as the select so single-variant cards stay aligned */
.swym-wishlist-variant-static {
flex: 1 0 100%;
box-sizing: border-box;
height: 38px;
padding: 0 10px;
border: 1px solid transparent;
border-radius: 6px;
font-size: 0.9375rem;
color: #6b7280;
display: flex;
align-items: center;
}
/* --- Cart / remove buttons -------------------------------- */
.swym-wishlist-cart-btn {
flex: 1;
padding: 9px 12px;
background: #111827;
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.9375rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
white-space: nowrap;
}
.swym-wishlist-cart-btn:hover:not(:disabled) {
background: #374151;
}
.swym-wishlist-cart-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.swym-wishlist-remove-btn {
flex-shrink: 0;
width: 38px;
height: 38px;
padding: 0;
background: none;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-size: 1.25rem;
line-height: 1;
cursor: pointer;
color: #6b7280;
transition: color 0.15s, border-color 0.15s;
}
.swym-wishlist-remove-btn:hover {
color: #ef4444;
border-color: #fca5a5;
}
/* --- Share modal ------------------------------------------ */
#swym-share-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.25rem;
}
#swym-share-modal-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #111827;
}
#swym-share-modal-close {
padding: 4px 8px;
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: #6b7280;
border-radius: 4px;
transition: background 0.15s, color 0.15s;
}
#swym-share-modal-close:hover {
background: #f3f4f6;
color: #111827;
}
.swym-share-section {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 1.25rem;
}
.swym-share-section label {
font-size: 0.9375rem;
font-weight: 500;
color: #374151;
}
.swym-share-section input,
.swym-share-section textarea {
padding: 9px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
color: #111827;
background: #fff;
resize: vertical;
}
.swym-share-section input:focus,
.swym-share-section textarea:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
.swym-share-section textarea {
min-height: 72px;
}
#swym-share-email-btn {
align-self: flex-start;
padding: 10px 20px;
background: #111827;
color: #fff;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
}
#swym-share-email-btn:hover:not(:disabled) {
background: #374151;
}
#swym-share-email-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
#swym-share-email-feedback,
#swym-share-copy-feedback {
font-size: 0.9375rem;
color: #16a34a;
font-weight: 500;
}
.swym-share-divider-label {
margin: 0 0 8px;
font-size: 0.9375rem;
color: #9ca3af;
text-align: center;
position: relative;
}
.swym-share-divider-label::before,
.swym-share-divider-label::after {
content: '';
position: absolute;
top: 50%;
width: 38%;
height: 1px;
background: #e5e7eb;
}
.swym-share-divider-label::before { left: 0; }
.swym-share-divider-label::after { right: 0; }
.swym-share-social-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.swym-share-social-btn,
#swym-share-copy-btn {
flex: 1;
padding: 9px 14px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
font-size: 0.9375rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
white-space: nowrap;
}
.swym-share-social-btn:hover,
#swym-share-copy-btn:hover:not(:disabled) {
background: #f3f4f6;
border-color: #9ca3af;
}
#swym-share-copy-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.swym-wishlist-collections-v2-container {
display: none !important;
}The selectors used across the apparel setup are:
| Selector | Purpose |
|---|---|
#swym-list-switcher-wrapper | Wrapper for the list <select> (hidden unless shopper has 2+ lists with Multiple Wishlists enabled) |
#swym-list-switcher | The <select> element for switching between lists |
.swym-wishlist-card | Individual product card |
.swym-wishlist-card-actions | Actions container inside each card |
.swym-wishlist-variant-label | Label above the variant <select> |
.swym-wishlist-variant-select | Variant dropdown - present only when product has 2+ variants |
.swym-wishlist-cart-btn | Add to Cart button; [disabled] = out of stock or no in-stock variant selected |
.swym-wishlist-remove-btn | Remove from wishlist button |
#swym-share-btn | Share Wishlist button in the header row |
#swym-share-modal | Share modal overlay wrapper |
#swym-share-modal-content | Share modal panel |
#swym-share-modal-header | Share modal title row |
.swym-share-section | Share modal content section (email form, social row) |
.swym-share-social-row | Row of social share buttons |
.swym-share-social-btn | Individual social share button |
.swym-share-divider-label | "or Share via" label between sections |
#swym-login-nudge | Sign-in prompt for anonymous users |
#swym-save-nudge | "Don't lose your list" banner for anonymous users with 3+ items |