Free code · No app required

Shopify wishlist without an app — the complete code

A wishlist on Shopify does not need an app. This page gives you the whole thing: a heart button for your product cards, the JavaScript that saves selections, and a wishlist page that pulls live prices from Shopify. It runs on localStorage and Shopify’s public /products/{handle}.js endpoint — no backend, no API key, no monthly fee. Roughly 2 KB of JavaScript. Every file below passes Shopify’s own Theme Check.

What this does, and what it does not

This implementation saves product handles in the visitor's browser and re-fetches each product when the wishlist page loads. Because only the handle is stored, the price, image and availability a customer sees are always current — a wishlist that caches prices will show stale numbers after a sale.

WorksDoes not work
Guests, with no account and no signupSyncing across a customer's phone and laptop
Survives reloads and browser restartsSurviving a cleared browser or private window
Live price and stock on the wishlist pageEmailing a customer when a saved item drops in price
Any Online Store 2.0 themeVintage (pre-2.0) themes without edits

The three 'does not work' items all need the list stored against a customer account rather than a browser. That is a genuinely different build, and it is the point at which paying someone starts to make sense. Everything above the line is free and on this page.

The code

Five files. Copy each one into the path shown. Nothing here calls an external service.

snippets/wishlist-button.liquid

The heart button. Drop it into any product card or the product page.

{%- comment -%}
  Wishlist button — no app, no backend.
  Usage:  {% render 'wishlist-button', product: product %}
{%- endcomment -%}

<button
  type="button"
  class="wl-btn"
  data-wl-handle="{{ product.handle }}"
  data-wl-id="{{ product.id }}"
  aria-label="Add {{ product.title | escape }} to wishlist"
  aria-pressed="false"
>
  <svg class="wl-icon" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
    <path d="M12 21s-7.5-4.6-9.6-8.6C.6 9.1 2.2 5.5 5.6 4.7c2-.5 3.9.3 5 1.8l1.4 1.9 1.4-1.9c1.1-1.5 3-2.3 5-1.8 3.4.8 5 4.4 3.2 7.7C19.5 16.4 12 21 12 21z"
          fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
  </svg>
</button>
assets/wishlist.js

All the logic: saves handles to localStorage, keeps every heart on the page in sync, updates the header count.

(function () {
  'use strict';
  var KEY = 'shopify_wishlist';

  function read() {
    try { return JSON.parse(localStorage.getItem(KEY)) || []; }
    catch (e) { return []; }
  }
  function write(list) {
    localStorage.setItem(KEY, JSON.stringify(list));
    // Same-tab listeners (the storage event only fires in OTHER tabs).
    document.dispatchEvent(new CustomEvent('wishlist:change', { detail: list }));
  }

  function toggle(handle) {
    var list = read();
    var i = list.indexOf(handle);
    if (i > -1) list.splice(i, 1); else list.push(handle);
    write(list);
    return i === -1; // true = it was just added
  }

  function paintOne(btn, list) {
    var on = list.indexOf(btn.dataset.wlHandle) > -1;
    btn.classList.toggle('is-active', on);
    btn.setAttribute('aria-pressed', on ? 'true' : 'false');
  }

  function paintAll() {
    var list = read();
    document.querySelectorAll('.wl-btn').forEach(function (b) { paintOne(b, list); });
    document.querySelectorAll('[data-wl-count]').forEach(function (el) {
      el.textContent = list.length;
      el.hidden = list.length === 0;
    });
  }

  // Delegated, so buttons added later (quick view, infinite scroll, section
  // re-render after a theme-editor change) work without re-binding.
  document.addEventListener('click', function (e) {
    var btn = e.target.closest('.wl-btn');
    if (!btn) return;
    e.preventDefault();
    toggle(btn.dataset.wlHandle);
    paintAll();
  });

  document.addEventListener('wishlist:change', paintAll);
  window.addEventListener('storage', function (e) { if (e.key === KEY) paintAll(); });
  document.addEventListener('DOMContentLoaded', paintAll);
  // Shopify fires this after a section re-renders in the theme editor.
  document.addEventListener('shopify:section:load', paintAll);
})();
assets/wishlist.css

Minimal styling. Uses currentColor so it inherits your theme.

.wl-btn {
  background: none;
  border: 0;
  padding: 6px;
  line-height: 0;
  cursor: pointer;
  color: inherit;
  transition: transform .15s ease;
}
.wl-btn:hover { transform: scale(1.12); }
.wl-btn.is-active .wl-icon path { fill: currentColor; }

.wl-count[hidden] { display: none; }
.wl-count {
  display: inline-block;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 9px;
  font-size: 12px;
  line-height: 18px;
  text-align: center;
  background: currentColor;
}

.wl-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 24px;
}
.wl-empty { padding: 48px 0; text-align: center; opacity: .7; }
sections/wishlist-page.liquid

The wishlist page itself. Reads the saved handles and pulls live product data from Shopify's own /products/{handle}.js — so price and availability are never stale.

<div class="page-width">
  <h1>{{ section.settings.heading | default: 'Your wishlist' }}</h1>
  <div class="wl-grid" id="wl-grid"></div>
  <p class="wl-empty" id="wl-empty" hidden>
    Nothing saved yet. Tap the heart on any product to add it here.
  </p>
</div>

<script>
(function () {
  var grid  = document.getElementById('wl-grid');
  var empty = document.getElementById('wl-empty');
  var money = {{ shop.money_format | json }};

  function fmt(cents) {
    return money.replace(/\{\{\s*amount\s*\}\}/, (cents / 100).toFixed(2));
  }

  async function render() {
    var handles = [];
    try { handles = JSON.parse(localStorage.getItem('shopify_wishlist')) || []; } catch (e) {}
    grid.innerHTML = '';
    empty.hidden = handles.length > 0;

    for (const handle of handles) {
      try {
        // Public JSON for any product. No API key, no app.
        const res = await fetch('/products/' + handle + '.js');
        if (!res.ok) continue;            // product deleted or unpublished
        const p = await res.json();
        const img = p.featured_image || (p.images && p.images[0]) || '';
        const card = document.createElement('div');
        card.className = 'wl-card';
        card.innerHTML =
          '<a href="/products/' + p.handle + '">' +
            (img ? '<img src="' + img + '&width=400" alt="" loading="lazy" width="400">' : '') +
            '<h3>' + p.title + '</h3>' +
          '</a>' +
          '<p>' + fmt(p.price) + '</p>' +
          '<button class="wl-btn is-active" data-wl-handle="' + p.handle + '" aria-pressed="true">Remove</button>';
        grid.appendChild(card);
      } catch (e) { /* skip this one, keep rendering the rest */ }
    }
  }

  document.addEventListener('DOMContentLoaded', render);
  document.addEventListener('wishlist:change', render);
})();
</script>

{% schema %}
{
  "name": "Wishlist page",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "Your wishlist" }
  ],
  "presets": [{ "name": "Wishlist page" }]
}
{% endschema %}
snippets/wishlist-count.liquid

Optional. The little counter for your header.

<a href="/pages/wishlist" class="wl-link" aria-label="Wishlist">
  Wishlist <span class="wl-count" data-wl-count hidden>0</span>
</a>

Want all 5 files as a download?

Every file above, already in the right theme folder, plus a README with the install steps. Unzip it straight into your theme instead of copying 5 blocks by hand.

One download with every file in its correct theme folder. We'll also email you when the next free guide ships — one click to unsubscribe, and we never pass your address on. The code above stays free whether you enter an email or not.

Installing it, step by step

  1. 1Add the three asset files

    In your theme, go to Online Store → Themes → … → Edit code. Create snippets/wishlist-button.liquid, assets/wishlist.js and assets/wishlist.css, and paste the code from each block above.

  2. 2Load the JS and CSS

    Open layout/theme.liquid and add these two lines just before </head>:

    {{ 'wishlist.css' | asset_url | stylesheet_tag }}
    <script src="{{ 'wishlist.js' | asset_url }}" defer></script>
  3. 3Put the heart on your product cards

    In Dawn this is snippets/card-product.liquid; in Horizon it is blocks/product-card.liquid. Find the block that renders the product image and add the render tag inside it. On the product page, add the same line to sections/main-product.liquid next to the Add to cart button.

    {% render 'wishlist-button', product: product %}
  4. 4Create the wishlist page

    Add sections/wishlist-page.liquid with the code above. Then in the admin create a page with the handle "wishlist" and assign it a template that renders this section. The URL becomes /pages/wishlist.

  5. 5Optional — header counter

    Add snippets/wishlist-count.liquid and render it in your header snippet where the cart icon lives:

    {% render 'wishlist-count' %}

Questions people actually ask

Does a Shopify wishlist work without an app?

Yes. A Shopify wishlist works without an app by storing the saved product handles in the browser's localStorage and reading live product data from Shopify's public /products/{handle}.js endpoint. No backend, no app subscription and no API key is needed. The trade-off is that the wishlist lives in that one browser: it survives page reloads and browser restarts, but it does not follow the customer to another device. If you need cross-device sync, that requires storing the list against the customer account, which does need either an app or a small backend.

Where is the wishlist data stored?

In localStorage under the key shopify_wishlist, as a plain JSON array of product handles — for example ["blue-tshirt","red-cap"]. Only handles are stored, never prices or images, which is deliberate: the wishlist page re-fetches each product on load, so a price change, a sale, or an out-of-stock flag is always current. Storing a price snapshot is the most common mistake in DIY wishlist tutorials and it shows customers stale numbers.

Will this work on the Dawn and Horizon themes?

Yes, on both, and on any Online Store 2.0 theme. The only file that differs is the product card. In Dawn the card is snippets/card-product.liquid; in Horizon it is blocks/product-card.liquid. Everything else — the JS, the CSS, the wishlist page section — is theme-independent because it hooks on a CSS class rather than on theme markup. The click handler is delegated from document, so hearts inside quick-view modals and infinite-scroll pages work with no extra binding.

Does a wishlist affect Core Web Vitals?

This implementation adds roughly 2 KB of JavaScript and no blocking requests, so the effect on LCP is negligible. The wishlist page does fetch one request per saved product, but that happens after the page has already rendered. The common performance mistake is loading a wishlist app on every page of the store to power a heart icon — that typically costs 40–100 KB plus a third-party connection on templates that never use it.

Can guests use the wishlist, or do they have to log in?

Guests can use it immediately, with no account. That is the main advantage of the localStorage approach and the main reason it converts: you are not asking for a signup before someone has decided to buy. The cost is that the list is per-browser. A common middle path is to keep localStorage for guests and, when a customer does log in, copy the list into a customer metafield so it follows them — that part does need a small app or a Shopify Function.

When this stops being enough

The code above covers a normal storefront wishlist. Teams come to us when they need the parts it deliberately leaves out — each one needs the list stored against the customer rather than the browser, which is a different build.

We build Shopify themes and apps, and we have shipped three of our own to the Shopify App Store. Tell us what you are trying to do and our team will go through the requirement with you — what it actually involves, what you do not need, and the simplest way to get there. This is paid work, and we will set it up at the lowest cost it can be done properly for. You will know the price before anything starts.

  • The list following a customer across devices
  • Back-in-stock or price-drop emails on saved items
  • Wishlist data in the admin, so merchandising can see it
  • Sharing a wishlist by link
Or book a 15-min call

Reply within a few hours · We go through the requirement with you first