How to Add a Wishlist to Shopify Without an App
You can add a functional wishlist to Shopify without paying a monthly app fee. The trade-off is setup time: you'll create a page template, wire up a button, and store saved products in customer metafields. Budget an afternoon for a first build. This guide walks through the five steps in order, using Liquid, the Shopify Ajax API, and metafields — all available on every Shopify plan.
Why Build a Native Shopify Wishlist Instead of Paying for an App
Wishlist apps typically charge a recurring monthly fee, and many inject their own scripts, styles, and markup into your theme. That matters for two reasons:
- Page speed. Every third-party script competes with your product images and checkout for bandwidth. A native build adds one small JavaScript file you control.
- Design control. App widgets come with their own CSS. Matching them to your theme's typography and spacing often means fighting
!importantrules. A native build inherits your theme's styles automatically.
There's also a data-ownership argument. A native wishlist stores data in Shopify metafields and customer tags, so it lives inside your store. If you switch themes or stop paying for an app, your data stays put.
The honest counterpoint: apps handle edge cases — guest wishlists, email reminders, cross-device sync for logged-out visitors — that take real engineering to replicate. If you need those, an app is the faster path. The native approach below covers the core case well: logged-in customers saving products and returning to buy them.
What You'll Need Before You Start (Theme Access and a Backup)
Before touching any theme code:
- Theme access. Either Shopify admin access with theme editing permissions, or a collaborator account with the "Themes" permission enabled. If you're working with a developer, invite them as a collaborator rather than sharing your admin login.
- A development theme. In Shopify admin, go to Online Store → Themes, click the three dots next to your live theme, and choose Duplicate. Work on the duplicate. Publish only after testing.
- A backup copy. Duplicate the theme again and download the
.zipas a rollback point. If something breaks, you can re-upload it in minutes. - A test customer account. Create a customer account in your store with a real email you control, so you can verify that wishlist data persists between sessions.
- Comfort with Liquid. You don't need to be an expert, but you should understand
{% if %},{% for %}, and how to referenceproductandcustomerobjects. Shopify's Liquid reference documents all of these.
One structural note before you start: this build assumes your theme uses JSON templates (the default in Online Store 2.0 themes like Dawn). If you're on an older theme with .liquid templates, the steps still work but you'll create the template file directly rather than through the theme editor.
Step 1: Create a Wishlist Page Template in Your Theme
- In your duplicated theme, go to Online Store → Themes → Customize.
- In the top bar, open the Pages dropdown and choose Add page — or create the page first under Online Store → Pages → Add page, titled "Wishlist", with handle
wishlist. - Back in the theme editor, click Add template in the template selector, choose page as the base, and name it
wishlist. Shopify createstemplates/page.wishlist.json. - In the theme code editor (Themes → Edit code), find the new JSON template. It will reference a section — create
sections/wishlist-page.liquidand point the template at it.
Inside sections/wishlist-page.liquid, start with a container and a schema block so merchants can edit the heading:
<div class="wishlist-page" data-wishlist-page>
<h1>{{ section.settings.heading }}</h1>
<div class="wishlist-grid" data-wishlist-grid></div>
<p class="wishlist-empty" data-wishlist-empty hidden>
{{ section.settings.empty_text }}
</p>
</div>
{% schema %}
{
"name": "Wishlist",
"settings": [
{ "type": "text", "id": "heading", "label": "Heading", "default": "My Wishlist" },
{ "type": "text", "id": "empty_text", "label": "Empty message", "default": "You haven't saved any products yet." }
],
"presets": [{ "name": "Wishlist" }]
}
{% endschema %}
Assign this template to the Wishlist page under Online Store → Pages → Wishlist → Theme template.
Step 2: Add the Wishlist Button to Product Pages and Collection Cards
Add a button to sections/main-product.liquid (or your theme's product form section) and to your product card snippet, usually snippets/product-card.liquid or snippets/card-product.liquid.
<button
type="button"
class="wishlist-toggle"
data-wishlist-toggle
data-product-id="{{ product.id }}"
data-product-handle="{{ product.handle }}"
aria-pressed="false"
aria-label="Save {{ product.title }} to wishlist">
<span class="wishlist-icon" aria-hidden="true">♡</span>
<span class="wishlist-label">Save</span>
</button>
Two practical notes:
- Use
data-attributes, not inlineonclick. It keeps your JavaScript in one file and avoids conflicts with theme scripts. - Include the handle, not just the ID. When you render the wishlist page, you'll need to fetch product data. Handles are cleaner for building URLs and for the Storefront API if you go that route later.
For collection cards, the same markup works — just make sure product is in scope inside your {% for product in collection.products %} loop.
Step 3: Store Wishlist Items with Metafields and Customer Tags
For logged-in customers, metafields are the right storage layer. Create a customer metafield definition under Settings → Custom data → Customers → Add definition:
| Setting | Value |
|---|---|
| Namespace and key | custom.wishlist |
| Type | List of single line text |
| Storefront access | Enabled (if you plan to read it client-side) |
| Description | Product handles saved to the customer's wishlist |
Then handle the save action in JavaScript. The simplest reliable pattern is to POST to your own app proxy or a Shopify Function endpoint — but for a no-app build, a small script that calls /account endpoints or uses the Storefront API with a customer access token works. If you want to avoid customer authentication entirely, use localStorage as a fallback and sync to metafields on login.
Here's the client-side toggle logic:
document.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-wishlist-toggle]');
if (!btn) return;
const id = btn.dataset.productId;
const handle = btn.dataset.productHandle;
const saved = JSON.parse(localStorage.getItem('wishlist') || '[]');
const index = saved.indexOf(handle);
if (index > -1) {
saved.splice(index, 1);
btn.setAttribute('aria-pressed', 'false');
} else {
saved.push(handle);
btn.setAttribute('aria-pressed', 'true');
}
localStorage.setItem('wishlist', JSON.stringify(saved));
});
If you're using customer metafields, mirror this write to the metafield via your authenticated endpoint so the wishlist survives a browser change.
Step 4: Build the Wishlist Page Logic to Display Saved Products
On page load in sections/wishlist-page.liquid, read the saved handles and render product cards. You have two options:
- Server-side: read
customer.metafields.custom.wishlistin Liquid and loop throughall_products[handle]. Fast, but only works for logged-in customers and requires a page reload after every change. - Client-side: fetch each product via
/products/{handle}.jsand build cards in JavaScript. Works for guests, updates instantly, but costs one request per product.
A hybrid works well: render server-side on first load, then update client-side. For most stores under a few hundred wishlist items, the client-side fetch is fine. Cap the number of fetches to avoid a slow page if someone has saved 200 products.
const handles = JSON.parse(localStorage.getItem('wishlist') || '[]');
const grid = document.querySelector('[data-wishlist-grid]');
if (!handles.length) {
document.querySelector('[data-wishlist-empty]').hidden = false;
} else {
for (const handle of handles.slice(0, 50)) {
const res = await fetch(`/products/${handle}.js`);
const product = await res.json();
// build and append a card using product.title, product.featured_image, product.price
}
}
Step 5: Style the Wishlist and Test It Across Devices
Style the grid with CSS Grid and make the toggle button's pressed state obvious:
.wishlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1.5rem;
}
.wishlist-toggle[aria-pressed="true"] .wishlist-icon {
color: #d6336c;
}
Then test these cases before publishing:
- Logged out, then logged in. Does the wishlist carry over, or does it reset?
- Two browsers. Save in Chrome, check in Safari. If it doesn't sync, your metafield write isn't working.
- Mobile Safari and Chrome on Android. Tap targets should be at least 44×44px.
- Empty state. Does the empty message actually show when nothing is saved?
- Theme editor. Change the heading in the editor and confirm it updates on the storefront.
Turning Wishlists into Repeat Purchases (and When an App Still Makes Sense)
A wishlist only earns money if you bring people back to it. Three tactics that work without any app:
- Add a wishlist link to your account menu so logged-in customers see it. Buried pages get no traffic.
- Tag customers who save items. Use a customer tag like
wishlist-activevia Shopify Flow, then trigger a Flow automation — for example, an email when a saved product goes on sale. - Surface saved items on the cart page with a "Still thinking about these?" block. It's a low-friction nudge at the moment of purchase.
Shopify Flow is included on higher plans; check current pricing for your plan tier before building automations on it.
When does an app still make sense? Choose one if you need guest wishlists with cross-device sync, automated back-in-stock or price-drop emails out of the box, or wishlist sharing via link. Those features require backend infrastructure — a database, scheduled jobs, email sending — that a theme-only build can't provide. For a store doing under a few hundred orders a month, the native build usually covers 80% of the value at 0% of the recurring cost.
Conclusion
A native Shopify wishlist is five connected pieces: a page template, a toggle button, a storage layer, a render loop, and styling. None of them are exotic, and all of them use Shopify features available on every plan. Build it on a duplicated theme, test the logged-out and logged-in paths, and you'll have a wishlist you fully control. If you later need guest sync or automated reminders, you can add an app on top without throwing away the work.
FAQ
Will this work on any Shopify theme?
It works on Online Store 2.0 themes that use JSON templates, including Dawn and most paid themes released since 2021. Older themes with .liquid templates need the same code but a different file structure — you'll create templates/page.wishlist.liquid directly instead of a JSON template.
Do wishlists work for customers who aren't logged in?
Yes, using localStorage. The limitation is that the list is tied to one browser on one device. It won't follow the customer to their phone or another browser unless they log in and you sync to customer metafields.
How many products can a customer save before performance suffers? The client-side approach fetches one request per product, so cap the render at 50 items and add a "load more" button beyond that. Server-side rendering with metafields handles larger lists better because it's a single page load.