Wishlist
A heart on every product card and on the product page saves a piece for later. Saved products appear in a header drawer and on a full wishlist page. No app, no account, no monthly fee.
Storage is the device’s, not the customer’s account. That’s a deliberate decision with real trade-offs, explained below.
Turning it on
Section titled “Turning it on”Theme settings → Quick view & wishlist
| Setting | Type | Default | What it does |
|---|---|---|---|
| Enable wishlist | Checkbox | On | Hearts on cards and product pages, the header count, and the drawer |
| Wishlist page | URL | (empty) | The wishlist page. Link it here so the header shows “Saved” |
To create the page: Online Store → Pages → Add page, then in the theme editor assign it the wishlist template. Point wishlist_url at it. Until you do, the drawer’s empty-state button and the header heart both fall back to /pages/wishlist.
Where it appears
Section titled “Where it appears”| Place | What the shopper gets |
|---|---|
| Product card | A heart in the card corner, filled when saved |
| Product page | A heart beside the add-to-bag button, from the Add to cart block |
| Header | A heart with a count, hidden entirely at zero |
| Wishlist drawer | Up to 12 saved pieces as rows, with add-to-bag and remove |
| Wishlist page | Every saved piece as a full product card |
Hearts are state-synced across the whole page: toggling one on a card immediately updates the same product’s heart in the quick view modal or anywhere else it’s visible.
Why it isn’t synced to the customer account
Section titled “Why it isn’t synced to the customer account”Because a theme cannot write it.
A wishlist tied to a customer account has to be stored against that customer, and a theme cannot write to a customer’s record. It can read and display what is there, but the Storefront API doesn’t expose customer metafield writes to a theme, and the Admin API requires a server-side credential no theme can safely hold. There is no version of an account-synced wishlist that is implementable in theme code.
So the options are: a real app with a server behind it, or the browser’s own storage. Signal takes the second and is honest about it — the wishlist page says “Saved on this device” rather than implying a sync that isn’t happening.
What you get, and what you don’t
Section titled “What you get, and what you don’t”| Signal’s wishlist | A wishlist app | |
|---|---|---|
| Cost | Included | Monthly subscription |
| Works for guests | Yes, immediately | Usually requires an account |
| Cross-device sync | No | Yes |
| Email reminders | No | Yes |
| ”Back in stock” on saved items | No | Often |
| Merchant-visible analytics | No | Yes |
| Setup | Already shipped | App install and configuration |
| Data leaving the device | None | Account and email |
For most drop-driven stores the device-local version is plenty: a shopper who cares enough to save something usually does it on the phone they intend to buy from, inside the same week. If re-engagement email is your actual strategy, install an app — and switch enable_wishlist off so there aren’t two hearts on every card.
How it works
Section titled “How it works”Saved products are held by the shopper’s own browser as a simple list of product handles:
["tactical-shell-coal", "ripstop-pack-30l", "balaclava-orange"]Handles only — no prices, no images, no timestamps. Every surface looks the product up fresh, so a price change, a rename or a sell-out is reflected the next time the list is opened rather than being frozen at the moment of saving.
Click handling is delegated at the document level, which is why hearts work on content injected after page load — load-more results, the quick view modal, recommendation rails.
The drawer
Section titled “The drawer”The header heart opens a drawer rather than navigating. Rows are fetched from /products/{handle}.js each time the drawer opens, capped at 12, so the list is always current and a shopper who never opens it pays nothing at page load.
A handle that answers with a 4xx is the product itself saying it’s been deleted, unpublished or renamed — those are dropped from the saved list. A network failure is not treated the same way, so a bad moment on the connection can’t quietly empty someone’s wishlist.
The heart in the header is a real link to the wishlist page, so it still goes somewhere useful even if scripting is unavailable.
The wishlist page
Section titled “The wishlist page”The page doesn’t hand-roll its own row markup. It turns the saved handles into a search query, re-fetches its own section through the Section Rendering API, and lifts out the server-rendered product cards:
/search?type=product&q=handle:tactical-shell-coal OR handle:ripstop-pack-30l§ion_id=…The result is that the wishlist looks exactly like every other product grid in the theme and inherits everything those cards do — badges, hover image swap, quick view, compare, and the heart itself, which doubles as the remove control. The returned set is verified against what was asked for, so a fuzzy search match can never put a product on the page the shopper didn’t save.
Cards are re-ordered into the saved order, newest first.
Accessibility
Section titled “Accessibility”- Every heart is a real
<button>witharia-pressedreflecting saved state, so a screen reader announces the toggle correctly rather than reading a decorative icon. - Its
aria-labelnames the product — “Save Tactical Shell for later” — so a list of hearts isn’t a list of identical “Save” buttons. - The drawer is
role="dialog"witharia-modal, closes on Escape, and locks page scroll while open. - The saved state is a filled icon, not a colour change alone.
Locale keys
Section titled “Locale keys”Under wishlist in locales/en.default.json: save_product, remove, remove_short, saved_locally, view_all, empty_a, empty_b, empty_text, empty_button.