"Go to Wishlist Button" Snippet
Read this article to learn more about the snippet usage.
Usage:
{% render 'wl-go-to-btn' %}
Code:
{% comment %}
Snippet: wl-go-to-btn
Usage:
{% render 'wl-go-to-btn' %}
{% render 'wl-go-to-btn',
icon_color: '#e53e3e',
text_color: '#333333',
icon_size: 24,
icon_size_mobile: 20,
text_size: 16,
text_size_mobile: 14,
show_text: false,
show_counter: true,
count_position: 'on_icon',
hide_counter_when_empty: true,
count_bg_color: '#e53e3e',
count_text_color: '#ffffff',
count_size: 18,
count_size_mobile: 16,
count_font_size: 10,
count_font_size_mobile: 10,
icon_text_gap: 6,
margin_top: 0,
margin_right: 0,
margin_bottom: 0,
margin_left: 0,
max_width: 0
%}
Parameters (all optional, defaults shown above):
icon_color — icon stroke/fill color
text_color — link text color
icon_size — icon size in px (desktop)
icon_size_mobile — icon size in px (mobile)
text_size — font size in px (desktop)
text_size_mobile — font size in px (mobile)
show_text — show link text (true/false)
show_counter — show wishlist count badge (true/false)
count_position — 'on_icon' | 'left' | 'right'
hide_counter_when_empty — hide badge when 0 items (true/false)
count_bg_color — badge background color
count_text_color — badge text color
count_size — badge circle size in px (desktop)
count_size_mobile — badge circle size in px (mobile)
count_font_size — badge font size in px (desktop)
count_font_size_mobile— badge font size in px (mobile)
icon_text_gap — gap between icon and text in px
margin_top/right/bottom/left — outer margins in px
max_width — max-width of the link in px (0 = none)
{% endcomment %}
{% liquid
assign _icon_color = icon_color | default: '#dcdcdcff'
assign _text_color = text_color | default: '#dcdcdcff'
assign _icon_size = icon_size | default: 20
assign _icon_size_mobile = icon_size_mobile | default: 20
assign _text_size = text_size | default: 14
assign _text_size_mobile = text_size_mobile | default: 14
assign _show_text = show_text | default: false
assign _show_counter = show_counter | default: true
assign _count_position = count_position | default: 'on_icon'
assign _hide_counter_when_empty = hide_counter_when_empty | default: true
assign _count_bg_color = count_bg_color | default: '#dcdcdcff'
assign _count_text_color = count_text_color | default: '#000000ff'
assign _count_size = count_size | default: 18
assign _count_size_mobile = count_size_mobile | default: 18
assign _count_font_size = count_font_size | default: 10
assign _count_font_size_mobile = count_font_size_mobile | default: 10
assign _icon_text_gap = icon_text_gap | default: 6
assign _margin_top = margin_top | default: 0
assign _margin_right = margin_right | default: 0
assign _margin_bottom = margin_bottom | default: 0
assign _margin_left = margin_left | default: 0
assign _max_width = max_width | default: 0
assign _snippet_id = 'wl-go-' | append: section.id | append: '-' | append: block.id
assign is_rtl = false
if request.locale.iso_code == 'ar' or request.locale.iso_code == 'he' or request.locale.iso_code == 'fa' or request.locale.iso_code == 'ur'
assign is_rtl = true
endif
assign dir_value = 'ltr'
if is_rtl
assign dir_value = 'rtl'
endif
%}
{% comment %} ── CSS (injected once per page via snippet-id guard) ────────── {% endcomment %}
<style id='wl-go-btn-styles' data-wl-go-styles>
.hidden {
display: none !important;
}
.wl-add a,
.wl-page a,
a.wl-go {
text-decoration: none;
}
button::before,
button::after {
content: none !important;
}
.wl-go {
display: inline-flex;
flex-wrap: nowrap;
justify-content: var(--wl-go-align, center);
align-items: center;
gap: var(--wl-go-gap, 8px);
transition: opacity 0.2s;
margin: var(--wl-go-margin-top, 0) var(--wl-go-margin-right, 0)
var(--wl-go-margin-bottom, 0) var(--wl-go-margin-left, 0);
width: 100%;
max-width: var(--wl-go-max-width, none);
color: var(--wl-go-text-color, #000);
font-size: var(--wl-go-text-size, 14px);
}
.wl-go:hover {
opacity: 0.8;
}
.wl-go:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.wl-go__icon {
display: flex;
flex-shrink: 0;
justify-content: center;
align-items: center;
color: var(--wl-go-icon-color, #000);
width: var(--wl-go-icon-size, 20px);
height: var(--wl-go-icon-size, 20px);
}
.wl-go__icon svg {
width: 100%;
height: 100%;
}
.wl-go__icon--counter-left,
.wl-go__icon--counter-right {
gap: 0.25em;
}
.wl-go__icon--counter-left .wl-go__counter {
order: -1;
}
.wl-go__icon--counter-on_icon {
position: relative;
}
.wl-go__text {
color: var(--wl-go-text-color, #000);
font-size: var(--wl-go-text-size, 14px);
}
.wl-go__counter {
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 999px;
background-color: var(--wl-go-count-bg-color, #000);
width: fit-content;
min-width: var(--wl-go-count-size, 18px);
height: var(--wl-go-count-size, 18px);
color: var(--wl-go-count-color, #fff);
font-weight: 600;
font-size: var(--wl-go-count-font-size, 10px);
}
.wl-go__icon--counter-on_icon .wl-go__counter {
position: absolute;
top: -4px;
inset-inline-end: -4px;
}
.wl-go__counter--empty {
display: none;
}
:dir(rtl) .wl-go__icon--counter-left .wl-go__counter {
order: 1;
}
:dir(rtl) .wl-go__icon--counter-right .wl-go__counter {
order: -1;
}
</style>
{% comment %} ── Per-instance CSS custom properties ───────────────────────── {% endcomment %}
<style>
#{{ _snippet_id }} .wl-go {
--wl-go-icon-color: {{ _icon_color }};
--wl-go-text-color: {{ _text_color }};
--wl-go-gap: {{ _icon_text_gap }}px;
--wl-go-margin-top: {{ _margin_top }}px;
--wl-go-margin-right: {{ _margin_right }}px;
--wl-go-margin-bottom: {{ _margin_bottom }}px;
--wl-go-margin-left: {{ _margin_left }}px;
--wl-go-icon-size: {{ _icon_size_mobile }}px;
--wl-go-text-size: {{ _text_size_mobile }}px;
--wl-go-count-bg-color: {{ _count_bg_color }};
--wl-go-count-color: {{ _count_text_color }};
--wl-go-count-size: {{ _count_size_mobile }}px;
--wl-go-count-font-size: {{ _count_font_size_mobile }}px;
{% if _max_width > 0 %}--wl-go-max-width: {{ _max_width }}px;{% endif %}
}
@media screen and (min-width: 768px) {
#{{ _snippet_id }} .wl-go {
--wl-go-icon-size: {{ _icon_size }}px;
--wl-go-text-size: {{ _text_size }}px;
--wl-go-count-size: {{ _count_size }}px;
--wl-go-count-font-size: {{ _count_font_size }}px;
}
}
</style>
{% comment %} ── Markup ────────────────────────────────────────────────────── {% endcomment %}
<span id='{{ _snippet_id }}'>
<a
href='#'
class='wl-go'
data-wishlist-go-link
dir='{{ dir_value }}'
{% if _show_counter %}
data-wishlist-counter-hide-when-empty='{{ _hide_counter_when_empty }}'
{% endif %}
>
<span class='wl-go__icon{% if _show_counter %} wl-go__icon--counter-{{ _count_position }}{% endif %}'>
<svg
xmlns='http://www.w3.org/2000/svg'
fill='none'
viewBox='0 0 24 24'
aria-hidden='true'
focusable='false'
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M7.87891 3.75c.67013.00152 1.33423.14051 1.9541.40918.61989.26872 1.18439.66253 1.66019 1.16016.1399.14633.3336.22972.5361.23144.2024.00163.3967-.07878.5391-.22266.9658-.9764 2.2584-1.51357 3.5957-1.50488 1.3372.00873 2.6231.5627 3.5771 1.55176.955.98998 1.5 2.33576 1.5088 3.74707.0087 1.41133-.52 2.76473-1.4629 3.76753l-6.6318 6.877c-.3018.3128-.7055.4834-1.1201.4834-.4146-.0001-.8175-.1707-1.1192-.4834l-6.63963-6.8828-.00196-.0029-.17578-.1885c-.3984-.452-.71987-.9742-.94922-1.543-.26198-.6498-.39789-1.34784-.39941-2.0537-.0015-.70608.13124-1.40537.39062-2.05664.25939-.6512.64026-1.24084 1.11817-1.73633.47787-.49543 1.04403-.88648 1.66504-1.15234.62086-.26578 1.28506-.40188 1.95508-.40039"
/>
</svg>
{% if _show_counter %}
<span
class='wl-go__counter wl-go__counter--empty'
data-wishlist-count
aria-hidden='true'
></span>
{% endif %}
</span>
{% if _show_text %}
<span class='wl-go__text' data-wishlist-go-text></span>
{% endif %}
</a>
</span>
{% comment %} ── JavaScript (injected once per page via id guard) ──────────── {% endcomment %}
<script>
(function () {
'use strict';
// Prevent double-init across multiple renders of this snippet
if (window.__wlGoSnippetInit) return;
window.__wlGoSnippetInit = true;
const SELECTORS = {
counter: '[data-wishlist-count]',
text: '[data-wishlist-go-text]',
link: '[data-wishlist-go-link]',
};
const DEBOUNCE_MS = 80;
const namespace = (window.__refactorWishlistApp =
window.__refactorWishlistApp || {});
// ── Helpers ──────────────────────────────────────────────────────────────
const logger = { warn: (...a) => console.warn('[Wishlist]', ...a) };
// Use the same namespace key and same method names as add-to-wishlist-btn
// so whichever script runs first, the cached object is compatible with both.
const getWishlistRuntimeHelpers = () => {
if (!namespace.runtimeHelpers) {
namespace.runtimeHelpers = {
getErrorMessage(error) {
return error instanceof Error ? error.message : String(error ?? 'Unknown error');
},
isFunction(value) {
return typeof value === 'function';
},
createDebouncedFunction(callback, delayMs) {
let timerId = null;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => callback(...args), delayMs);
};
},
resolveWishlistInstance() {
const helper = window.refactor_apps?.wishlist ?? null;
if (helper) return { helper, error: null };
return { helper: null, error: new Error('WishlistHelper is not available') };
},
ensureHelperContractOrThrow(helper, methods) {
for (const methodName of methods) {
if (typeof helper?.[methodName] === 'function') continue;
throw new Error(`WishlistHelper method is missing: ${methodName}`);
}
},
};
}
return namespace.runtimeHelpers;
};
const {
getErrorMessage,
isFunction,
createDebouncedFunction,
resolveWishlistInstance,
ensureHelperContractOrThrow,
} = getWishlistRuntimeHelpers();
// ── Wishlist adapter ──────────────────────────────────────────────────────
const createAdapter = () => {
let readyPromise = null;
const getHelper = async () => {
if (readyPromise) return readyPromise;
readyPromise = (async () => {
const { helper, error } = resolveWishlistInstance();
if (error) throw error;
if (isFunction(helper.ready)) await helper.ready();
return helper;
})().catch((e) => { readyPromise = null; throw e; });
return readyPromise;
};
return {
ready: async () => { await getHelper(); },
getItems: async () => {
const h = await getHelper();
ensureHelperContractOrThrow(h, ['getUniqueWishlistItems']);
return h.getUniqueWishlistItems({ forceSync: true }) ?? [];
},
};
};
// ── State ─────────────────────────────────────────────────────────────────
let linkEntries = [];
let inFlight = null;
const adapter = createAdapter();
const cleanups = [];
// ── Resolve href + text from wishlist app ─────────────────────────────────
const resolveLinkMeta = () => {
try {
const wl = window.refactor_apps?.wishlist;
return {
href: wl?.linkUrl || '#',
text: wl?.linkText || '',
};
} catch (_) {
return { href: '#', text: '' };
}
};
const applyLinkMeta = () => {
const { href, text } = resolveLinkMeta();
for (const { link } of linkEntries) {
if (href && href !== '#') link.setAttribute('href', href);
if (text) link.setAttribute('aria-label', text);
const textEl = link.querySelector(SELECTORS.text);
if (textEl && text) textEl.textContent = text;
}
};
// ── DOM ───────────────────────────────────────────────────────────────────
const collect = () => {
linkEntries = [...document.querySelectorAll(SELECTORS.link)].map((link) => ({
link,
counter: link.querySelector(SELECTORS.counter),
}));
};
const updateCounter = (el, count, hideWhenEmpty) => {
const hide = count === 0 && hideWhenEmpty;
el.textContent = hide ? '' : String(count);
el.classList.toggle('wl-go__counter--empty', hide);
};
const updateAll = (count) => {
applyLinkMeta();
for (const { link, counter } of linkEntries) {
link.classList.toggle('wl-go--active', count > 0);
if (!counter) continue;
const hideWhenEmpty =
link.getAttribute('data-wishlist-counter-hide-when-empty') === 'true';
updateCounter(counter, count, hideWhenEmpty);
}
};
// ── Update cycle ──────────────────────────────────────────────────────────
const runUpdate = async () => {
if (inFlight) return inFlight;
const { helper } = resolveWishlistInstance();
if (!helper) { updateAll(0); return; }
inFlight = (async () => {
try {
await adapter.ready();
const items = await adapter.getItems();
updateAll(items.length);
} catch (e) {
logger.warn('wl-go-to-btn: failed to update counter', getErrorMessage(e));
updateAll(0);
} finally {
inFlight = null;
}
})();
return inFlight;
};
const reloadUpdate = async () => {
collect();
await runUpdate();
};
// ── Teardown ──────────────────────────────────────────────────────────────
const teardown = () => {
cleanups.forEach((fn) => fn());
cleanups.length = 0;
linkEntries = [];
inFlight = null;
window.__wlGoSnippetInit = false;
};
const on = (target, event, handler) => {
target.addEventListener(event, handler);
cleanups.push(() => target.removeEventListener(event, handler));
};
// ── Init ──────────────────────────────────────────────────────────────────
const init = () => {
collect();
void runUpdate();
const debouncedUpdate = createDebouncedFunction(runUpdate, DEBOUNCE_MS);
on(window, 'wishlist-updated', debouncedUpdate);
on(window, 'wishlist:updated', debouncedUpdate);
on(window, 'wishlist-initialized', debouncedUpdate);
on(document, 'shopify:section:load', reloadUpdate);
on(document, 'turbo:before-cache', teardown);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
Do you need help?
If you have any questions or run into issues, please contact us — we’re happy to help.