Headless Components
Two approaches
Pre-rendered Liquid (recommended) — paste HTML that Shopify renders on the server. Components appear instantly with zero JS delay. JS only runs after load to hydrate wishlist state (fill/unfill the heart) and attach click handlers. This is the best option for performance.
Custom elements — paste a <stashlist-header-icon>, <stashlist-collection-item>, or <stashlist-pdp-button> tag. The JS finds it after load, renders the component inside, and sets display: contents on the host. Slightly simpler to paste but introduces a brief render delay.
Header icon
Open sections/header.liquid and paste the icon HTML exactly where you want it to appear. Adjust the CSS variable values to match your theme's colours and sizes.
Pre-rendered Liquid (recommended)
Outputs the icon directly as server-rendered HTML. No JS delay. In drawer mode, Stashlist automatically upgrades the link to open the drawer on click.
<!-- Paste where you want the icon in sections/header.liquid -->
<a class="stashlist-header-icon"
aria-label="Wishlist"
href="/pages/wishlist"
style="position: relative; display: inline-flex; align-items: center; justify-content: center;">
<svg class="stashlist-heart-icon"
xmlns="http://www.w3.org/2000/svg"
width="20" height="20" viewBox="0 0 24 24"
style="width:20px; height:20px; flex-shrink:0; stroke: var(--color-foreground); stroke-width: 1.5px;"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path class="stashlist-heart-path"
style="fill:none;"
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
<span class="stashlist-header-count"
data-stashlist-counter="0"
style="display:none; position: absolute; bottom: 19px; right: 1px;
min-width: 20px; height: 20px; border-radius: 50%; padding: 0 3px;
box-sizing: border-box; justify-content: center; align-items: center; text-align: center;
background: var(--cart-bubble-background, var(--cart-bubble-background-fallback));
color: var(--cart-bubble-text, var(--cart-bubble-text-fallback));">
</span>
</a>
<style>
.stashlist-header-icon:hover { transform: scale(1.07); }
</style> style attributes, and inline any custom CSS you add too. Everything in the snippet's style attributes is there so the icon renders correctly before Stashlist's stylesheet loads, size, position, color, stroke. If you add your own CSS on top (via a theme stylesheet or a <style> block) to override size, color, or position, add matching inline values to the element as well. Otherwise the icon renders with browser defaults first, then visibly snaps or shifts once your CSS loads. Only pure hover/transition effects are safe to leave external.
Custom element (simpler, slight delay)
JS finds the element after load and renders the icon inside it. Simpler to paste, but the icon appears after JS runs rather than instantly.
<!-- Paste where you want the icon -->
<stashlist-header-icon
data-icon-size="20"
data-icon-stroke-color="var(--color-foreground)"
data-stroke-width="1.5"
data-show-count="true"
data-counter-size="20"
data-counter-position="bottom-right"
data-counter-offset-x="1"
data-counter-offset-y="19"
data-counter-bg-color="var(--cart-bubble-background, var(--cart-bubble-background-fallback))"
data-counter-text-color="var(--cart-bubble-text, var(--cart-bubble-text-fallback))">
</stashlist-header-icon>
<style>
.stashlist-header-icon:hover { transform: scale(1.07); }
</style> Worked example: Horizon v3.3.1
Confirmed live on a real Horizon 3.3.1 store. Every value below is Horizon's own real token or a measured pixel value, not a placeholder, paste this exactly as-is into snippets/header-actions.liquid, as a sibling of <cart-drawer-component>.
<a class="stashlist-header-icon stashlist-header-icon--headless"
aria-label="Wishlist"
href="/pages/wishlist"
style="position: relative; width: 44px; display: inline-flex; align-items: center; justify-content: center;">
<svg class="stashlist-heart-icon"
xmlns="http://www.w3.org/2000/svg"
width="20" height="20" viewBox="0 0 24 24"
style="width:20px; height:20px; flex-shrink:0; stroke: black; stroke-width: 1.8px;"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path class="stashlist-heart-path"
style="fill:none;"
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
<span class="stashlist-header-count" data-stashlist-counter="0"
style="position: absolute;
top: 5px;
right: 1px;
min-width: 20px;
height: 20px;
padding: 0px 3px;
background: var(--color-primary-button-background);
font-family: var(--font-paragraph--family);
font-weight: var(--font-paragraph--weight);
font-size: var(--font-size--3xs);
box-shadow: 0 0 0 var(--input-box-shadow-width) var(--color-primary-button-text);
border-radius: 100px;
color: var(--color-primary-button-text);
align-items: center;
display: none;
justify-content: center;"
aria-live="polite"></span>
</a>
<style>
/* Belt-and-suspenders: forces the hidden/circular default no matter what
ends up in the span's own inline style="" above, a copy-paste accident
there can't cause a visible square flash anymore. */
.stashlist-header-count { border-radius: 100px !important; }
.stashlist-header-count[data-stashlist-counter=""],
.stashlist-header-count[data-stashlist-counter="0"] { display: none !important; }
</style> header-actions reactively after some cart events (confirmed: Add to Cart from the PDP), which can visually reset this counter to its blank template state. Stashlist's JS self-heals this automatically as of the 2026-08-29 release, no theme-side workaround needed, just make sure the Stashlist Settings embed is on a current version.
body, and Shopify renders body-targeted embeds at the end of the page, after your header already painted.
To hide it instantly with no flash, wrap the whole snippet yourself in
{% if customer %}...{% endif %}. Note this then always hides it for guests, it stops following the app toggle entirely, changing your mind later means editing this Liquid again, not flipping the setting.
Header icon attributes
Icon
| Attribute | Values | What it does |
|---|---|---|
data-icon-size | Number (px) | Width and height of the SVG icon. |
data-icon-stroke-color | Hex, rgb, or CSS variable | Stroke colour in the default (unsaved) state. Pass var(--color-foreground) to inherit your theme's text colour automatically. |
data-stroke-width | Number | SVG stroke width. 1.5 matches most theme header icons. |
data-extra-class | CSS class name | Additional class added to the rendered anchor element. Use this to inherit spacing or layout classes from your theme. |
Item count badge
| Attribute | Values | What it does |
|---|---|---|
data-show-count | true | false | Whether to show the item count badge. |
data-counter-size | Number (px) | Width and height of the count badge. |
data-counter-position | top-right | top-left | bottom-right | bottom-left | Which corner of the icon the badge is anchored to. |
data-counter-offset-x | Number (px) | Horizontal offset of the badge from its corner anchor. |
data-counter-offset-y | Number (px) | Vertical offset of the badge from its corner anchor. |
data-counter-bg-color | Hex, rgb, or CSS variable | Badge background colour. Pass a CSS variable to inherit the theme's cart bubble colour. |
data-counter-text-color | Hex, rgb, or CSS variable | Badge number colour. |
Collection grid buttons
Open your product card snippet (e.g. snippets/card-product.liquid) and paste the wishlist button where you want it. All product data is output by Liquid — no JS fetch required.
Pre-rendered Liquid — floating badge (recommended)
Paste inside your image container element. The parent must have position: relative for the badge to position correctly. Adjust top, right, and z-index to suit your card layout.
<!-- Paste inside your image container. Ensure the parent has position:relative. -->
<div class="stashlist-wishlist-btn-wrapper stashlist-wishlist-btn-wrapper--card"
style="position:absolute; top:8px; right:8px; z-index:10;"
data-product-id="{{ product.id }}"
data-variant-id="{{ product.selected_or_first_available_variant.id }}"
data-product-title="{{ product.title | escape }}"
data-variant-title="{{ product.selected_or_first_available_variant.title | escape }}"
data-product-url="{{ product.url }}"
data-image-url="{{ product.featured_image | img_url: '400x' }}"
data-price="{{ product.price | money_without_currency }}"
data-variant-sku="{{ product.selected_or_first_available_variant.sku | escape }}">
<button class="stashlist-wishlist-btn" type="button" aria-label="Add to wishlist">
<svg class="stashlist-heart-icon" xmlns="http://www.w3.org/2000/svg"
width="20" height="20" viewBox="0 0 24 24"
style="width:20px; height:20px;"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path class="stashlist-heart-path"
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>
</div> Pre-rendered Liquid — inline button (recommended)
Paste wherever you want the button to appear in the document flow.
<div class="stashlist-wishlist-btn-wrapper stashlist-wishlist-btn-wrapper--card"
data-product-id="{{ product.id }}"
data-variant-id="{{ product.selected_or_first_available_variant.id }}"
data-product-title="{{ product.title | escape }}"
data-variant-title="{{ product.selected_or_first_available_variant.title | escape }}"
data-product-url="{{ product.url }}"
data-image-url="{{ product.featured_image | img_url: '400x' }}"
data-price="{{ product.price | money_without_currency }}"
data-variant-sku="{{ product.selected_or_first_available_variant.sku | escape }}">
<button class="stashlist-wishlist-btn" type="button" aria-label="Add to wishlist">
<svg class="stashlist-heart-icon" xmlns="http://www.w3.org/2000/svg"
width="18" height="18" viewBox="0 0 24 24"
style="width:18px; height:18px;"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path class="stashlist-heart-path"
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>
</div> Custom element (simpler, slight delay)
<!-- Floating: paste inside your image container element -->
<stashlist-collection-item
data-product-id="{{ product.id }}"
data-variant-id="{{ product.selected_or_first_available_variant.id }}"
data-product-title="{{ product.title | escape }}"
data-variant-title="{{ product.selected_or_first_available_variant.title | escape }}"
data-product-url="{{ product.url }}"
data-image-url="{{ product.featured_image | img_url: '400x' }}"
data-price="{{ product.price | money_without_currency }}"
data-variant-sku="{{ product.selected_or_first_available_variant.sku | escape }}"
data-mode="floating"
data-badge-position="top-right"
data-badge-offset="8"
data-badge-size="20"
data-badge-z-index="10"
data-icon-stroke-color="currentColor">
</stashlist-collection-item> <stashlist-collection-item> is automatically set to position: relative if it is not already, so the floating badge positions correctly.
Worked example: Horizon v3.3.1
Confirmed live on a real Horizon 3.3.1 store. Paste inside each product card's wrapper in your card snippet (e.g. card-gallery.liquid), the card needs position: relative.
<div
class="stashlist-wishlist-btn-wrapper stashlist-wishlist-btn-wrapper--card stashlist-preset--horizon"
style="position:absolute;z-index:10;top:8px;right:8px;"
data-product-id="{{ product.id }}"
data-variant-id="{{ product.selected_or_first_available_variant.id }}"
data-product-title="{{ product.title | escape }}"
data-product-url="{{ product.url }}"
data-image-url="{{ product.featured_image | image_url: width: 600 }}"
data-price="{{ product.price | money_without_currency }}"
>
<button class="stashlist-wishlist-btn" type="button" aria-label="Add to wishlist">
<svg class="stashlist-heart-icon" xmlns="http://www.w3.org/2000/svg" width="14.67" height="14.67" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path class="stashlist-heart-path" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
</button>
</div>
<style>
/* Unconditional reset, applies at every width — without this the raw
button shows the browser's own default chrome (grey background, border,
padding) until wishlist.css loads async, confirmed live on mobile.
More specific than wishlist.css's own .stashlist-wishlist-btn rule, so
it keeps winning even after that file arrives, load order doesn't matter. */
.stashlist-wishlist-btn-wrapper.stashlist-preset--horizon .stashlist-wishlist-btn {
background: transparent;
border: none;
padding: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: relative;
isolation: isolate;
}
.stashlist-wishlist-btn-wrapper.stashlist-preset--horizon .stashlist-heart-icon {
stroke: black;
stroke-width: 1.5px;
}
.stashlist-wishlist-btn-wrapper.stashlist-preset--horizon .stashlist-heart-path {
fill: none;
}
@media screen and (min-width: 750px) {
.stashlist-wishlist-btn-wrapper.stashlist-preset--horizon {
opacity: 0;
transition: opacity 0.2s ease;
}
.product-card:hover .stashlist-wishlist-btn-wrapper.stashlist-preset--horizon {
opacity: 1;
}
.stashlist-wishlist-btn-wrapper.stashlist-preset--horizon .stashlist-wishlist-btn {
border: 2px solid hsl(0 0% 0% / .15);
border-radius: 100px;
height: 36px;
width: 36px;
right: 4px;
background: #fff;
}
.stashlist-wishlist-btn-wrapper.stashlist-preset--horizon .stashlist-heart-icon {
stroke-width: 2.5px;
}
}
</style> <style> block once, not inside the product loop, it applies to every card via the class selector. It's a real <style> tag directly in your Liquid template, not a linked stylesheet, so it's parsed synchronously with the rest of the page, no flash-of-visible-then-hidden on page load the way an app-loaded async stylesheet would cause.
Collection item attributes
Product data (required)
| Attribute | Liquid value | What it does |
|---|---|---|
data-product-id | {{ product.id }} | Shopify product ID. |
data-variant-id | {{ product.selected_or_first_available_variant.id }} | Default variant ID. Used when multi-variant mode is off. |
data-product-title | {{ product.title | escape }} | Product title, stored with the wishlist item. |
data-variant-title | {{ product.selected_or_first_available_variant.title | escape }} | Variant title (e.g. "Small / Black"). |
data-product-url | {{ product.url }} | Product URL, used in wishlist page and alert emails. |
data-image-url | {{ product.featured_image | img_url: '400x' }} | Product image URL shown on the wishlist page. |
data-price | {{ product.price | money_without_currency }} | Price used for price drop detection. |
data-variant-sku | {{ product.selected_or_first_available_variant.sku | escape }} | Variant SKU, stored with the item for reporting. |
Appearance
| Attribute | Values | What it does |
|---|---|---|
data-mode | floating | inline | Floating overlays the badge on the product image. Inline renders the button in the document flow. |
data-badge-position | top-right | top-left | bottom-right | bottom-left | Floating mode: which corner of the image container the badge is anchored to. |
data-badge-offset | Number (px) | Floating mode: distance from each edge at the chosen corner. |
data-badge-size | Number (px) | Floating mode: width and height of the heart icon. |
data-badge-z-index | Number | Z-index of the badge. Increase if it appears behind other card elements like Quick Add buttons. |
data-icon-size | Number (px) | Inline mode: width and height of the icon. |
data-icon-stroke-color | Hex, rgb, or CSS variable | Icon stroke colour. currentColor inherits from the surrounding text. |
data-icon-fill-color | Hex, rgb, or CSS variable | Fill colour when the item is saved. |
Product page button
Open the snippet that renders your product form (e.g. sections/main-product.liquid or snippets/buy-buttons.liquid) and paste the button where you want it. When a customer selects a different variant, Stashlist updates data-variant-id automatically via the variant:changed event.
Pre-rendered Liquid (recommended)
Outputs the button instantly as server-rendered HTML. Adjust the CSS variables in the inline style to match your theme.
data-product-id is required on the wrapper div. Without it, Stashlist cannot locate the wrapper in the DOM and will fall back to a parent element that carries no variant information, causing multi-variant state to malfunction.
<!-- Paste where you want the wishlist button in your product form -->
<div class="stashlist-wishlist-btn-wrapper stashlist-wishlist-btn-wrapper--inline"
data-product-id="{{ product.id }}"
data-variant-id="{{ product.selected_or_first_available_variant.id }}"
data-product-title="{{ product.title | escape }}"
data-variant-title="{{ product.selected_or_first_available_variant.title | escape }}"
data-product-url="{{ product.url }}"
data-image-url="{{ product.featured_image | img_url: '800x' }}"
data-price="{{ product.price | money_without_currency }}"
data-variant-sku="{{ product.selected_or_first_available_variant.sku | escape }}">
<button class="stashlist-wishlist-btn stashlist-wishlist-btn--with-text"
type="button"
aria-label="Add to wishlist"
style="--stashlist-height: 44px;
--stashlist-border-width: 1px;
--stashlist-border-color: rgba(var(--color-foreground), 0.3);
--stashlist-border-radius: 4px;
--stashlist-padding: 0 12px;
--stashlist-stroke-width: 1.5px;">
<svg class="stashlist-heart-icon" xmlns="http://www.w3.org/2000/svg"
width="18" height="18" viewBox="0 0 24 24"
style="width:18px; height:18px; flex-shrink:0;"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path class="stashlist-heart-path"
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
<span class="stashlist-wishlist-btn__text stashlist-wishlist-btn__text--add">Add to wishlist</span>
<span class="stashlist-wishlist-btn__text stashlist-wishlist-btn__text--added">In wishlist</span>
</button>
</div> <span> elements and change the button class to just stashlist-wishlist-btn. Add --stashlist-width: 44px; to the inline style to make it square.
Custom element (simpler, slight delay)
Product data is read automatically from window.__stashlistEmbedProduct, which the Stashlist Settings embed populates on every product page.
<stashlist-pdp-button
data-show-text="true"
data-text-add="Add to wishlist"
data-text-added="In wishlist"
data-button-height="44"
data-show-border="true"
data-border-width="1"
data-border-color="rgba(var(--color-foreground), 0.3)"
data-border-radius="4"
data-icon-size="18"
data-stroke-width="1.5">
</stashlist-pdp-button> Worked example: Horizon v3.3.1
Confirmed live on a real Horizon 3.3.1 store. Wrap the rendered add-to-cart-button snippet and this button together in a flex row, in add-to-cart.liquid.
main-product.liquid) instead of the shared snippet, otherwise the wishlist button shows up in quick-add popups too.
<div class="stashlist-flex" style="display: flex;">
{% render 'add-to-cart-button',
id: id,
class: class,
can_add_to_cart: can_add_to_cart,
product: closest.product,
add_to_cart_text: add_to_cart_text,
data_testid: 'standalone-add-to-cart'
%}
<div
class="stashlist-wishlist-btn-wrapper stashlist-wishlist-btn-wrapper--inline stashlist-wishlist-btn-wrapper--headless-pdp"
data-product-id="{{ product.id }}"
data-variant-id="{{ product.selected_or_first_available_variant.id }}"
data-product-title="{{ product.title | escape }}"
data-product-url="{{ product.url }}"
data-image-url="{{ product.featured_image | image_url: width: 600 }}"
data-variant-sku="{{ product.selected_or_first_available_variant.sku }}"
data-variant-barcode="{{ product.selected_or_first_available_variant.barcode }}"
style="display:inline-flex; width:52px; min-width:52px; vertical-align:middle; margin-left:10px;"
>
<button
type="button"
class="stashlist-wishlist-btn"
aria-label="Add to wishlist"
style="display:flex; align-items:center; justify-content:center;
width:100%; padding:0 12px;
background:transparent; color:inherit; cursor:pointer;
position:relative; isolation:isolate; box-sizing:border-box;
border-style:solid; border-width:1px; border-color:currentColor;
border-radius:14px; box-shadow:none;"
>
<svg class="stashlist-heart-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" style="stroke:currentColor; stroke-width:1.4px;" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path class="stashlist-heart-path" style="fill:none;" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
</button>
</div>
</div> stashlist-wishlist-btn-wrapper--headless-pdp is an escape hatch, not decorative. It stops the PDP's "sync wrapper to currently-selected variant" click delegation from overwriting this wrapper's data-variant-id in layouts where that logic could otherwise target the wrong element. Keep the class even if it looks unused.
PDP button attributes
| Attribute | Values | What it does |
|---|---|---|
data-show-text | true | false | Whether to show "Add to wishlist" / "In wishlist" text alongside the icon. |
data-text-add | String | Label shown when the item is not yet in the wishlist. Defaults to "Add to wishlist". |
data-text-added | String | Label shown when the item is already saved. Defaults to "In wishlist". |
data-button-height | Number (px) | Fixed height of the button. Match this to your add-to-cart button height. |
data-button-width | Number (px) | Fixed width. Use for icon-only square buttons. |
data-show-border | true | false | Whether to show a border around the button. |
data-border-width | Number (px) | Border thickness. |
data-border-color | Hex, rgb, or CSS variable | Border colour. Accepts CSS variables to match your theme automatically. |
data-border-radius | Number (px) | Border radius. 100 produces a pill/circle shape. |
data-background-color | Hex, rgb, or CSS variable | Button background colour. |
data-icon-size | Number (px) | Width and height of the heart icon. |
data-stroke-width | Number | SVG stroke width. |
data-icon-stroke-color | Hex, rgb, or CSS variable | Icon stroke colour in the default (unsaved) state. |
data-font-size | Number (px) | Font size of the button label text. |
data-letter-spacing | Number (px) | Letter spacing of the button label text. |
data-margin-top | Number (px) | Top margin on the button wrapper. Use to add spacing between the button and the element above it. |
data-margin-bottom | Number (px) | Bottom margin on the button wrapper. |
Save for later (cart)
Unlike the components above, Save for later isn't a static placement, cart contents change constantly via AJAX and Shopify's own theme re-renders. Instead of pasting fully-styled markup, paste a bare marker button and Stashlist's JS finds it on each cart row and wires up the click behaviour, icon, and saved/unsaved state for you.
Open your cart line item template (e.g. cart-products.liquid) and paste this as a sibling of your theme's own remove button, never nested inside it, a <button> can't validly nest inside another <button> in real HTML.
<button
type="button"
data-stashlist-save-btn
aria-label="Save for later"
style="display:inline-flex; align-items:center; justify-content:center; background:none; border:none; padding:0; cursor:pointer; opacity:0.55; color:currentColor; -webkit-appearance:none; appearance:none;"
>
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:inline-block; vertical-align:middle; flex-shrink:0;">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>
The data-stashlist-save-btn attribute is just a marker, it needs no value. Stashlist resolves the variant from the cart row the button sits in (via that row's own id/data-key), not from anything on the button itself, so no product/variant data attributes are needed here. The heart icon shown is pre-rendered on purpose, matching the exact path Stashlist's own JS builds, this removes the "empty box, then icon pops in" flash you'd otherwise see waiting on the cart's own AJAX fetch, JS only toggles the icon's fill and the button's opacity once it resolves the saved state.
Variant capture
Variant capture stores the specific variant a customer saves (size, colour, etc.) rather than just the product. When using the Stashlist Settings app embed, enable it in the theme editor under App Embeds → Stashlist Settings → Variants.
Advanced: fully headless (no app embed)
If you cannot or do not want to use the Stashlist Settings app embed, you can add a stashlist-settings-config div manually anywhere on the page. This div replaces the app embed — it loads no additional assets, but tells the JS how to behave. You are responsible for loading wishlist.js and wishlist.css yourself in this mode.
Most merchants using headless components should use the app embed and skip this section.
| Attribute | Values | What it does |
|---|---|---|
data-wishlist-url | Any path, e.g. /pages/wishlist | The URL the header icon links to when wishlist mode is page. |
data-wishlist-mode | page | drawer | Set to drawer to open the wishlist drawer on icon click. Set to page to navigate to the wishlist URL. |
data-variant-mode | true | false | Enable per-variant wishlist tracking. When using the app embed, set this in the theme editor instead. |
data-icon-style | heart | heart-rounded | bookmark | custom | Icon shape used everywhere Stashlist renders a wishlist icon. |
data-custom-icon-svg | Raw SVG markup | Your custom SVG, used when data-icon-style="custom". Use a square viewBox, fill="none", stroke="currentColor", and add class="stashlist-heart-path" to the main path. |
Legacy: selector-based injection
If you cannot edit your theme's Liquid templates directly, you can use the original selector-based approach. Paste config divs anywhere in the page; the JS finds them by ID and automatically injects icons using CSS selectors to locate the target elements.
See the examples below for Tinker v4.1. For other themes, inspect your header and product card HTML to find the right selectors.
Header icon (Tinker v4.1)
<div id="stashlist-settings-config"
data-wishlist-url="/pages/wishlist"
data-wishlist-mode="drawer"
data-icon-style="heart"
style="display:none">
</div>
<div id="stashlist-header-config"
data-selector-primary="button[aria-controls='cart-drawer']"
data-position-primary="before"
data-second-injection="true"
data-selector-secondary="#Details-menu-drawer-container, .menu-drawer-container"
data-position-secondary="after"
data-wrap-flex-secondary="true"
data-responsive-switching="true"
data-icon-size="20"
data-icon-stroke-color="var(--color-foreground)"
data-extra-class="header-actions__action"
data-stroke-width="1.5"
data-show-count="true"
data-counter-size="20"
data-counter-position="bottom-right"
data-counter-offset-x="1"
data-counter-offset-y="19"
data-counter-bg-color="var(--cart-bubble-background, var(--cart-bubble-background-fallback))"
data-counter-text-color="var(--cart-bubble-text, var(--cart-bubble-text-fallback))"
data-counter-mobile-override="true"
data-counter-offset-x-mobile="1"
data-counter-offset-y-mobile="8"
style="display:none">
</div>
<style>
.stashlist-header-icon:hover { transform: scale(1.07); }
.stashlist-header-count {
font-family: var(--font-paragraph--family) !important;
font-weight: var(--font-paragraph--weight) !important;
font-size: var(--font-size--3xs) !important;
width: 20px !important;
height: 20px !important;
}
</style> Collection grid (Tinker v4.1, floating)
<div id="stashlist-collection-config"
data-mode="floating"
data-selector=".card-gallery > a"
data-badge-position="top-right"
data-badge-offset="8"
data-badge-size="18"
data-badge-z-index="10"
style="display:none">
</div> PDP button (Tinker v4.1, inline)
<div
id="stashlist-embed-config"
data-headless="true"
data-mode="inline"
data-selector=".product-form-buttons"
data-position="append"
data-button-width="52"
data-show-border="true"
data-border-color="var(--button-background-color)"
data-border-radius="100"
data-icon-size="18"
data-stroke-width="1.4"
data-background-color="var(--color-primary-button-background)"
style="display:none">
</div> data-headless="true" is required on the PDP config. If your store also has the Stashlist PDP embed app block installed, both output a stashlist-embed-config div. Without the headless flag, the JS reads the app block's config instead of yours.