Wishlist Page
Build a dedicated wishlist page with theme-native product cards, login nudge, conversion prompts, remove, add-to-cart, and wishlist sharing.
Wishlist Page
A dedicated page showing all wishlisted products with remove, add-to-cart, and sharing actions.
Single list or multiple lists? The implementation below handles both setups automatically.
setupListSwitchercallsswat.isCollectionsEnabled()at runtime and hides the list switcher when the Multiple Wishlists feature is off in your Swym app settings, or when the shopper has only one list. Enable or disable the feature in Swym app settings and the page adapts; no code changes needed.
Setup
Step 1 — Create the section file for your custom wishlist
In your theme code editor (Online Store > Themes > Edit code), create sections/swym-wishlist-page.liquid that will hold the HTML structure for the custom wishlist section:
Section Liquid — sections/swym-wishlist-page.liquid
sections/swym-wishlist-page.liquid{% comment %} Swym Wishlist Page — SDK-Only Mode {% 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>Step 2 — Create a page template for your custom wishlist
In your theme code editor (Online Store > Themes > Edit code), create templates/page.wishlist.json that references a new section:
{
"sections": {
"main": {
"type": "swym-wishlist-page",
"settings": {}
}
},
"order": ["main"]
}Step 3 — Create a Shopify page
In your Shopify admin: Online Store > Pages > Add page. Set the title to "Wishlist". In the Theme template dropdown (right sidebar), select page.wishlist.
Step 4 — Add the JavaScript asset
In your theme code editor (Online Store > Themes > Edit code), create assets/swym-wishlist-page.js in your theme with the JavaScript below.
Adapting Cards to Your Theme
Before adding the javascript, please remember that the wishlist page product cards must match your collection page's card layout. A shopper should not be able to tell the wishlist page was built separately.
Read your collection section file and identify:
- Image wrapper — aspect ratio,
object-fit, CSS classes - Title element — HTML tag, class, font styling
- Price element — format, class, currency symbol
- Card wrapper — classes, spacing
- Grid —
grid-template-columns,gap, responsive breakpoints
Then replicate that structure in the renderProducts function below. The default implementation uses generic .swym-wishlist-card classes — replace these with your theme's actual card classes.
JavaScript — 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) {
// Use the SDK's built-in login check — this is the canonical way.
// Under the hood, Swym's Liquid snippet sets window.swymCustomerId
// from Shopify's {{ customer.id }} object. The SDK reads it on init.
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;
// Show login nudge for anonymous users
if (!isLoggedIn) {
var loginNudge = document.getElementById('swym-login-nudge');
if (loginNudge) loginNudge.style.display = '';
}
// Start renderProducts before swat.fetchLists resolves; both run in parallel.
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;
}
}
// fetchLists gives us the default list lid (needed for sharing) and its
// contents in one call — no separate swat.fetch needed on initial load.
swat.fetchLists({
callbackFn: function(lists) {
if (loading) loading.style.display = 'none';
// Restore the previously-selected list from cache; fall back to lists[0].
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 || []) : [];
// Always update cache and re-render from the authoritative API response
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);
}
// Always initialize the switcher so it's wired up regardless of cache hit
setupListSwitcher(swat, lists, lid, grid, countEl, empty);
// Re-render on wishlist changes — refetch all lists so the switcher
// reflects updated counts and any list that became empty / was deleted.
if (swat.evtLayer) {
swat.evtLayer.addEventListener('sw:removedfromwishlist', function() {
swat.fetchLists({
callbackFn: function(freshLists) {
// Preserve whichever list was active; fall back to the first remaining one.
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');
});
}
}
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';
// Fetch live product data from Shopify in parallel before building any card.
// Results are cached by variant ID for the lifetime of the page so list switches
// don't re-fetch products already loaded. Cache is cleared on page refresh.
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; });
}));
// IMPORTANT: Replace the HTML below with your theme's collection card structure
// to match the look and feel of your collection page
products.forEach(function(product, index) {
var shopifyProduct = shopifyData[index];
var variant = shopifyProduct && shopifyProduct.variants
? shopifyProduct.variants.find(function(v) { return String(v.id) === String(product.epi); })
: null;
var inStock = variant ? variant.available : product.stk !== 0;
var displayPrice = variant ? variant.price : product.pr;
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 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">' +
'<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">\u00d7</button>' +
'</div>';
var cartBtn = card.querySelector('.swym-wishlist-cart-btn');
var removeBtn = card.querySelector('.swym-wishlist-remove-btn');
if (inStock) {
cartBtn.addEventListener('click', function() {
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: Number(product.epi), 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(err) { console.error('deleteFromList error', err); }
);
});
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 = '';
// Remove the previous listener by reference rather than replacing the node.
// Replacing the node triggers Swym's SDK button-watcher, which re-attaches its
// own listener and results in two openShareModal calls per click.
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;
// Reset form fields and feedback
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 {
// Must mark the list public before a shareable URL can be generated
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;
});
}
// Bind email button
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); });
}
// Close — button, overlay click, Escape key
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;
// Hide the switcher for shoppers with only one list or when Collections is disabled.
if (!swat.isCollectionsEnabled() || lists.length < 2) {
wrapper.style.display = 'none';
return;
}
wrapper.style.display = '';
// Replace the node to clear any listener attached during a previous fetchLists call.
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);
});
// Reflect the active list name in the page title on switcher setup.
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);
})();How It Works
-
On SDK ready, checks login state via
swat.platform.isLoggedIn()— shows the login nudge for anonymous users -
If a product list is cached in
sessionStorage(SS_PRODUCTS_KEY), the loading indicator is hidden andrenderProductsstarts beforeswat.fetchListsresolves, running both in parallel. Cards still wait for Shopify product JSON fetches to complete before painting.swat.fetchLists()always re-renders from the authoritative API response; cache is a UX optimisation, not a data source -
swat.fetchLists()returns both the default listlid(needed for sharing) andlistcontents(products) in a single call — no separateswat.fetch()needed on initial load -
Rendering products
Single list (default)
If empty, shows the empty state with a browse CTA. If products exist,renderProductsfetches{du}.jsfor every item in parallel viaPromise.allbefore building any card. Price and availability come from the matching Shopify variant; Swym metadata is the fallback if a fetch fails. Cards render once all fetches resolve.Multiple lists
swat.fetchLists()always returns the fulllistsarray.setupListSwitcheris called after every live render and handles both cases automatically: it checksswat.isCollectionsEnabled()and the shopper's list count at runtime, so no code changes are needed when switching between single and multi-list configurations. When Multiple Wishlists is enabled and the shopper has two or more lists, the switcher renders a<select>dropdown and re-renders the page with the selected list'slistcontentson change, with no additional API call. When conditions are not met, it hides itself. -
Save nudge: If an anonymous user has 3+ items, shows "Don't lose your list" once per session (dismissible, tracked in
sessionStorage) -
Remove calls
swat.deleteFromList(lid, ...), removes the card from DOM, updates the count, and invalidates the share URL cache so a fresh link is generated next time the modal opens -
Add to Cart POSTs to Shopify's
/cart/add.jswith the variant ID.aria-labelupdates at each stage ("Adding…", "Added!", "Failed to add…") so screen readers announce the outcome -
Share modal — "Share Wishlist" button (hidden until 1+ items) opens a modal with three options:
- Email — collects name, recipient email, and an optional message, then calls
swat.sendListViaEmail({ toEmailId, fromName, note, lid }). Shows "Email sent!" and auto-closes after 1 s - Copy link — calls
swat.markListPublic(lid)first (required before a shareable URL can be generated), thenswat.generateSharedListURL(lid). The resulting URL is cached insessionStorage; subsequent opens reuse it without another network call. Copies via Clipboard API withexecCommandfallback. Shows "Copied!" and auto-closes after 800 ms - Social (Facebook, X) — calls
swat.shareListSocial(lid, …, platform)and opens the platform share URL in a new tab
- Email — collects name, recipient email, and an optional message, then calls
-
Modal accessibility: focus moves to the first field on open, Tab key is trapped within the modal, Escape closes it, and focus returns to the "Share Wishlist" button on close
-
Listens for
sw:removedfromwishlistevents to re-render in real-time
Conversion Features
| Feature | When | What |
|---|---|---|
| Login nudge | Anonymous user visits wishlist page | Soft banner: "Sign in to save your wishlist across devices" |
| Save nudge | Anonymous user has 3+ items | "Don't lose your list! You've saved N items. Sign in to keep them." |
| Empty state | 0 items | "Start adding items you love" with browse CTA |
| Return URL | Login link | Always passes ?return_url=/pages/wishlist so user returns after login |
| Share | 1+ items in wishlist | "Share Wishlist" button opens a modal with email (sendListViaEmail), copy link (markListPublic → generateSharedListURL, cached in sessionStorage), and social sharing (shareListSocial) for Facebook and X |
Why 3 items? This threshold balances intent signals — users with 3+ items have demonstrated enough engagement that a save prompt feels helpful rather than intrusive.
Product Fields Used
Product data freshness
renderProductsfetches/{locale}/{product-url}.jsfor every wishlisted item in parallel before building any card. This endpoint is unauthenticated and locale-aware. The product data is read from the matching Shopify variant; Swym data serves as the fallback if a fetch fails.
| Field | Usage |
|---|---|
iu | Product image |
dt | Product title |
du | Product URL (links) - also used as the base for fetchProductJSON ({du}.js) |
pr | Price in cents - fallback when the Shopify fetch fails |
stk | Stock status - fallback when the Shopify fetch fails |
epi | Variant ID - used to find the matching Shopify variant and for the cart call |
empi | Product ID |
Styling
Match your theme exactly. The product cards, grid layout, typography, and button styles should be indistinguishable from your collection page. A shopper should not be able to tell this page was built separately. Read your collection section file and replicate its card structure in
renderProductsbefore shipping.
The reference table below maps each element to its role. If you need a starting point before integrating your theme's styles, a basic stylesheet is provided after the table.
| Element | Purpose |
|---|---|
#swym-wishlist-grid | Product grid container — match your collection grid's grid-template-columns, gap. Has aria-busy toggled during render |
.swym-wishlist-card | Product card — replace with your theme's card classes |
#swym-login-nudge | Login banner — style as a soft info banner |
#swym-save-nudge | Save prompt — aria-live="polite" region; style as a dismissible attention banner |
#swym-share-btn | Share button — hidden (display:none) until 1+ items load; style to match theme buttons |
#swym-share-modal | Modal root — position:fixed; inset:0; z-index:9999; hidden by default |
#swym-share-modal-overlay | Semi-transparent backdrop — clicking it closes the modal |
#swym-share-modal-content | Modal card — centered at top:50%; left:50%; transform:translate(-50%,-50%), max-width 480 px; replace background:#fff and border-radius:8px with your theme's CSS variables |
.swym-share-section | Each share option group (email form, social row) |
.swym-share-social-btn | Facebook and X share buttons — identify by data-platform attribute |
#swym-share-copy-btn | Copy link button — disabled until markListPublic + generateSharedListURL resolve |
#swym-share-copy-feedback | "Copied!" aria-live confirmation |
#swym-share-email-feedback | "Email sent!" aria-live confirmation |
#swym-wishlist-empty | Empty state — center-aligned with browse CTA |
Starter stylesheet — assets/swym-wishlist-page.css
assets/swym-wishlist-page.cssThis stylesheet is a generic starting point only. It uses hardcoded colours and sizes that will not match your theme. Replace every value that conflicts with your theme's design tokens before shipping.
/* ============================================================
Swym Wishlist Page
============================================================ */
/* --- Page layout ------------------------------------------ */
#swym-wishlist-page {
padding: 2rem 1rem 4rem;
}
#swym-wishlist-page h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
line-height: 1.2;
}
#swym-wishlist-count {
font-size: 1.125rem;
font-weight: 400;
color: #6b7280;
}
/* --- 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;
gap: 8px;
padding: 12px 14px 14px;
margin-top: auto;
}
.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;
}
.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;
}