Headless Components

Headless components let you place Stashlist's header icon, collection buttons, and PDP wishlist button directly in your theme's Liquid files. No selectors or app blocks required — you control exactly where each component appears.

Prerequisite: The Stashlist Settings app embed must be enabled in your theme editor. It loads the JS and CSS assets and handles global settings (wishlist mode, icon style, variant capture) so you don't have to configure them manually. Go to Online Store → Themes → Customize → App Embeds and toggle on Stashlist Settings.

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>
To show the icon on both desktop and mobile, paste the snippet in both locations in your header markup. Each instance works independently.
Keep the 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>
Horizon re-renders 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.
Using "Hide wishlist for guests"? By default this snippet shows for everyone, our JS hides it for guests when that setting is on, with a brief flash first, the Stashlist Settings app embed it reads from targets 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

AttributeValuesWhat 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

AttributeValuesWhat 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>
The parent element of <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>
Paste the <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)

AttributeLiquid valueWhat 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

AttributeValuesWhat 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.

Important: 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>
Icon-only button: Remove the two <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.

Check where this file is called from first. Some themes render the same add-to-cart snippet for both the real product page and a quick-add modal on collection pages. If yours does, paste this in the product-page-only caller (e.g. 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

AttributeValuesWhat 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.

Keep the Cart Integration app embed turned ON even in an otherwise fully headless setup. It's what loads the JS that finds and wires this button, this component has no fully-headless "no embed" mode the way Header/Collection/PDP do.

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.

AttributeValuesWhat 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.